const batch = require('../lib/batch'); const request = require('../lib/request'); const originalURL = '/parse/batch'; const serverURL = 'http://localhost:1234/parse'; const serverURL1 = 'http://localhost:1234/1'; const serverURLNaked = 'http://localhost:1234/'; const publicServerURL = 'http://domain.com/parse'; const publicServerURLNaked = 'http://domain.com/'; const publicServerURLLong = 'https://domain.com/something/really/long'; const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Installation-Id': 'yolo', }; describe('batch', () => { let createSpy; beforeEach(async () => { createSpy = spyOn(databaseAdapter, 'createObject').and.callThrough(); }); it('should return the proper url', () => { const internalURL = batch.makeBatchRoutingPathFunction(originalURL)('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url given a public url-only path', () => { const originalURL = '/something/really/long/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, serverURL, publicServerURLLong )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url given a server url-only path', () => { const originalURL = '/parse/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, serverURL, publicServerURLLong )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url same public/local endpoint', () => { const originalURL = '/parse/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, serverURL, publicServerURL )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with different public/local mount', () => { const originalURL = '/parse/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, serverURL1, publicServerURL )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with naked public', () => { const originalURL = '/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, serverURL, publicServerURLNaked )('/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with naked local', () => { const originalURL = '/parse/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, serverURLNaked, publicServerURL )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with no url provided', () => { const originalURL = '/parse/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, undefined, publicServerURL )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with no public url provided', () => { const originalURL = '/parse/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, serverURLNaked, undefined )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with bad url provided', () => { const originalURL = '/parse/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, 'badurl.com', publicServerURL )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with bad public url provided', () => { const originalURL = '/parse/batch'; const internalURL = batch.makeBatchRoutingPathFunction( originalURL, serverURLNaked, 'badurl.com' )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should handle a batch request without transaction', async () => { const response = await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', body: JSON.stringify({ requests: [ { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value2' }, }, ], }), }); expect(response.data.length).toEqual(2); expect(response.data[0].success.objectId).toBeDefined(); expect(response.data[0].success.createdAt).toBeDefined(); expect(response.data[1].success.objectId).toBeDefined(); expect(response.data[1].success.createdAt).toBeDefined(); const query = new Parse.Query('MyObject'); const results = await query.find(); expect(createSpy.calls.count()).toBe(2); expect(createSpy.calls.argsFor(0)[3]).toEqual(null); expect(createSpy.calls.argsFor(1)[3]).toEqual(null); expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); }); it('should handle a batch request with transaction = false', async () => { const response = await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', body: JSON.stringify({ requests: [ { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value2' }, }, ], transaction: false, }), }); expect(response.data.length).toEqual(2); expect(response.data[0].success.objectId).toBeDefined(); expect(response.data[0].success.createdAt).toBeDefined(); expect(response.data[1].success.objectId).toBeDefined(); expect(response.data[1].success.createdAt).toBeDefined(); const query = new Parse.Query('MyObject'); const results = await query.find(); expect(createSpy.calls.count()).toBe(2); expect(createSpy.calls.argsFor(0)[3]).toEqual(null); expect(createSpy.calls.argsFor(1)[3]).toEqual(null); expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); }); if ( process.env.MONGODB_TOPOLOGY === 'replicaset' || process.env.PARSE_SERVER_TEST_DB === 'postgres' ) { describe('transactions', () => { it('should handle a batch request with transaction = true', async () => { const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections await myObject.save(); await myObject.destroy(); createSpy.calls.reset(); const response = await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', body: JSON.stringify({ requests: [ { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value2' }, }, ], transaction: true, }), }); expect(response.data.length).toEqual(2); expect(response.data[0].success.objectId).toBeDefined(); expect(response.data[0].success.createdAt).toBeDefined(); expect(response.data[1].success.objectId).toBeDefined(); expect(response.data[1].success.createdAt).toBeDefined(); const query = new Parse.Query('MyObject'); const results = await query.find(); expect(createSpy.calls.count()).toBe(2); for (let i = 0; i + 1 < createSpy.calls.length; i = i + 2) { expect(createSpy.calls.argsFor(i)[3]).toBe( createSpy.calls.argsFor(i + 1)[3] ); } expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); }); it('should not save anything when one operation fails in a transaction', async () => { const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections await myObject.save({ key: 'stringField' }); await myObject.destroy(); createSpy.calls.reset(); try { // Saving a number to a string field should fail await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', body: JSON.stringify({ requests: [ { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 10 }, }, ], transaction: true, }), }); fail(); } catch (error) { expect(error).toBeDefined(); const query = new Parse.Query('MyObject'); const results = await query.find(); expect(results.length).toBe(0); } }); it('should generate separate session for each call', async () => { await reconfigureServer(); const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections await myObject.save({ key: 'stringField' }); await myObject.destroy(); const myObject2 = new Parse.Object('MyObject2'); // This is important because transaction only works on pre-existing collections await myObject2.save({ key: 'stringField' }); await myObject2.destroy(); createSpy.calls.reset(); let myObjectCalls = 0; Parse.Cloud.beforeSave('MyObject', async () => { myObjectCalls++; if (myObjectCalls === 2) { try { // Saving a number to a string field should fail await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', body: JSON.stringify({ requests: [ { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject2', body: { key: 10 }, }, ], transaction: true, }), }); fail('should fail'); } catch (e) { expect(e).toBeDefined(); } } }); const response = await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', body: JSON.stringify({ requests: [ { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject', body: { key: 'value2' }, }, ], transaction: true, }), }); expect(response.data.length).toEqual(2); expect(response.data[0].success.objectId).toBeDefined(); expect(response.data[0].success.createdAt).toBeDefined(); expect(response.data[1].success.objectId).toBeDefined(); expect(response.data[1].success.createdAt).toBeDefined(); await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', body: JSON.stringify({ requests: [ { method: 'POST', path: '/1/classes/MyObject3', body: { key: 'value1' }, }, { method: 'POST', path: '/1/classes/MyObject3', body: { key: 'value2' }, }, ], }), }); const query = new Parse.Query('MyObject'); const results = await query.find(); expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); const query2 = new Parse.Query('MyObject2'); const results2 = await query2.find(); expect(results2.length).toEqual(0); const query3 = new Parse.Query('MyObject3'); const results3 = await query3.find(); expect(results3.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); expect(createSpy.calls.count() >= 13).toEqual(true); let transactionalSession; let transactionalSession2; let myObjectDBCalls = 0; let myObject2DBCalls = 0; let myObject3DBCalls = 0; for (let i = 0; i < createSpy.calls.count(); i++) { const args = createSpy.calls.argsFor(i); switch (args[0]) { case 'MyObject': myObjectDBCalls++; if (!transactionalSession || (myObjectDBCalls - 1) % 2 === 0) { transactionalSession = args[3]; } else { expect(transactionalSession).toBe(args[3]); } if (transactionalSession2) { expect(transactionalSession2).not.toBe(args[3]); } break; case 'MyObject2': myObject2DBCalls++; if (!transactionalSession2 || (myObject2DBCalls - 1) % 9 === 0) { transactionalSession2 = args[3]; } else { expect(transactionalSession2).toBe(args[3]); } if (transactionalSession) { expect(transactionalSession).not.toBe(args[3]); } break; case 'MyObject3': myObject3DBCalls++; expect(args[3]).toEqual(null); break; } } expect(myObjectDBCalls % 2).toEqual(0); expect(myObjectDBCalls > 0).toEqual(true); expect(myObject2DBCalls % 9).toEqual(0); expect(myObject2DBCalls > 0).toEqual(true); expect(myObject3DBCalls % 2).toEqual(0); expect(myObject3DBCalls > 0).toEqual(true); }); }); } });