// These tests are unit tests designed to only test transform.js. 'use strict'; const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform'); const dd = require('deep-diff'); const mongodb = require('mongodb'); const Utils = require('../lib/Utils'); describe('parseObjectToMongoObjectForCreate', () => { it('a basic number', done => { const input = { five: 5 }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: { five: { type: 'Number' } }, }); jequal(input, output); done(); }); it('an object with null values', done => { const input = { objectWithNullValues: { isNull: null, notNull: 3 } }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: { objectWithNullValues: { type: 'object' } }, }); jequal(input, output); done(); }); it('built-in timestamps with date', done => { const input = { createdAt: '2015-10-06T21:24:50.332Z', updatedAt: '2015-10-06T21:24:50.332Z', }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {}, }); expect(output._created_at instanceof Date).toBe(true); expect(output._updated_at instanceof Date).toBe(true); done(); }); it('array of pointers', done => { const pointer = { __type: 'Pointer', objectId: 'myId', className: 'Blah', }; const out = transform.parseObjectToMongoObjectForCreate( null, { pointers: [pointer] }, { fields: { pointers: { type: 'Array' } }, } ); jequal([pointer], out.pointers); done(); }); //TODO: object creation requests shouldn't be seeing __op delete, it makes no sense to //have __op delete in a new object. Figure out what this should actually be testing. xit('a delete op', done => { const input = { deleteMe: { __op: 'Delete' } }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {}, }); jequal(output, {}); done(); }); it('Doesnt allow ACL, as Parse Server should tranform ACL to _wperm + _rperm', done => { const input = { ACL: { '0123': { read: true, write: true } } }; expect(() => transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }) ).toThrow(); done(); }); it('parse geopoint to mongo', done => { const lat = -45; const lng = 45; const geoPoint = { __type: 'GeoPoint', latitude: lat, longitude: lng }; const out = transform.parseObjectToMongoObjectForCreate( null, { location: geoPoint }, { fields: { location: { type: 'GeoPoint' } }, } ); expect(out.location).toEqual([lng, lat]); done(); }); it('parse polygon to mongo', done => { const lat1 = -45; const lng1 = 45; const lat2 = -55; const lng2 = 55; const lat3 = -65; const lng3 = 65; const polygon = { __type: 'Polygon', coordinates: [ [lat1, lng1], [lat2, lng2], [lat3, lng3], ], }; const out = transform.parseObjectToMongoObjectForCreate( null, { location: polygon }, { fields: { location: { type: 'Polygon' } }, } ); expect(out.location.coordinates).toEqual([ [ [lng1, lat1], [lng2, lat2], [lng3, lat3], [lng1, lat1], ], ]); done(); }); it('in array', done => { const geoPoint = { __type: 'GeoPoint', longitude: 180, latitude: -180 }; const out = transform.parseObjectToMongoObjectForCreate( null, { locations: [geoPoint, geoPoint] }, { fields: { locations: { type: 'Array' } }, } ); expect(out.locations).toEqual([geoPoint, geoPoint]); done(); }); it('in sub-object', done => { const geoPoint = { __type: 'GeoPoint', longitude: 180, latitude: -180 }; const out = transform.parseObjectToMongoObjectForCreate( null, { locations: { start: geoPoint } }, { fields: { locations: { type: 'Object' } }, } ); expect(out).toEqual({ locations: { start: geoPoint } }); done(); }); it('objectId', done => { const out = transform.transformWhere(null, { objectId: 'foo' }); expect(out._id).toEqual('foo'); done(); }); it('objectId in a list', done => { const input = { objectId: { $in: ['one', 'two', 'three'] }, }; const output = transform.transformWhere(null, input); jequal(input.objectId, output._id); done(); }); it('built-in timestamps', done => { const input = { createdAt: new Date(), updatedAt: new Date() }; const output = transform.mongoObjectToParseObject(null, input, { fields: {}, }); expect(typeof output.createdAt).toEqual('string'); expect(typeof output.updatedAt).toEqual('string'); done(); }); it('pointer', done => { const input = { _p_userPointer: '_User$123' }; const output = transform.mongoObjectToParseObject(null, input, { fields: { userPointer: { type: 'Pointer', targetClass: '_User' } }, }); expect(typeof output.userPointer).toEqual('object'); expect(output.userPointer).toEqual({ __type: 'Pointer', className: '_User', objectId: '123', }); done(); }); it('null pointer', done => { const input = { _p_userPointer: null }; const output = transform.mongoObjectToParseObject(null, input, { fields: { userPointer: { type: 'Pointer', targetClass: '_User' } }, }); expect(output.userPointer).toBeUndefined(); done(); }); it('file', done => { const input = { picture: 'pic.jpg' }; const output = transform.mongoObjectToParseObject(null, input, { fields: { picture: { type: 'File' } }, }); expect(typeof output.picture).toEqual('object'); expect(output.picture).toEqual({ __type: 'File', name: 'pic.jpg' }); done(); }); it('mongo geopoint to parse', done => { const lat = -45; const lng = 45; const input = { location: [lng, lat] }; const output = transform.mongoObjectToParseObject(null, input, { fields: { location: { type: 'GeoPoint' } }, }); expect(typeof output.location).toEqual('object'); expect(output.location).toEqual({ __type: 'GeoPoint', latitude: lat, longitude: lng, }); done(); }); it('mongo polygon to parse', done => { const lat = -45; const lng = 45; // Mongo stores polygon in WGS84 lng/lat const input = { location: { type: 'Polygon', coordinates: [ [ [lat, lng], [lat, lng], ], ], }, }; const output = transform.mongoObjectToParseObject(null, input, { fields: { location: { type: 'Polygon' } }, }); expect(typeof output.location).toEqual('object'); expect(output.location).toEqual({ __type: 'Polygon', coordinates: [ [lng, lat], [lng, lat], ], }); done(); }); it('bytes', done => { const input = { binaryData: 'aGVsbG8gd29ybGQ=' }; const output = transform.mongoObjectToParseObject(null, input, { fields: { binaryData: { type: 'Bytes' } }, }); expect(typeof output.binaryData).toEqual('object'); expect(output.binaryData).toEqual({ __type: 'Bytes', base64: 'aGVsbG8gd29ybGQ=', }); done(); }); it('nested array', done => { const input = { arr: [{ _testKey: 'testValue' }] }; const output = transform.mongoObjectToParseObject(null, input, { fields: { arr: { type: 'Array' } }, }); expect(Array.isArray(output.arr)).toEqual(true); expect(output.arr).toEqual([{ _testKey: 'testValue' }]); done(); }); it('untransforms objects containing nested special keys', done => { const input = { array: [ { _id: 'Test ID', _hashed_password: "I Don't know why you would name a key this, but if you do it should work", _tombstone: { _updated_at: "I'm sure people will nest keys like this", _acl: 7, _id: { someString: 'str', someNumber: 7 }, regularKey: { moreContents: [1, 2, 3] }, }, regularKey: 'some data', }, ], }; const output = transform.mongoObjectToParseObject(null, input, { fields: { array: { type: 'Array' } }, }); expect(dd(output, input)).toEqual(undefined); done(); }); it('changes new pointer key', done => { const input = { somePointer: { __type: 'Pointer', className: 'Micro', objectId: 'oft' }, }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: { somePointer: { type: 'Pointer' } }, }); expect(typeof output._p_somePointer).toEqual('string'); expect(output._p_somePointer).toEqual('Micro$oft'); done(); }); it('changes existing pointer keys', done => { const input = { userPointer: { __type: 'Pointer', className: '_User', objectId: 'qwerty', }, }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: { userPointer: { type: 'Pointer' } }, }); expect(typeof output._p_userPointer).toEqual('string'); expect(output._p_userPointer).toEqual('_User$qwerty'); done(); }); it('writes the old ACL format in addition to rperm and wperm on create', done => { const input = { _rperm: ['*'], _wperm: ['Kevin'], }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {}, }); expect(typeof output._acl).toEqual('object'); expect(output._acl['Kevin'].w).toBeTruthy(); expect(output._acl['Kevin'].r).toBeUndefined(); expect(output._rperm).toEqual(input._rperm); expect(output._wperm).toEqual(input._wperm); done(); }); it('removes Relation types', done => { const input = { aRelation: { __type: 'Relation', className: 'Stuff' }, }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: { aRelation: { __type: 'Relation', className: 'Stuff' }, }, }); expect(output).toEqual({}); done(); }); it('writes the old ACL format in addition to rperm and wperm on update', done => { const input = { _rperm: ['*'], _wperm: ['Kevin'], }; const output = transform.transformUpdate(null, input, { fields: {} }); const set = output.$set; expect(typeof set).toEqual('object'); expect(typeof set._acl).toEqual('object'); expect(set._acl['Kevin'].w).toBeTruthy(); expect(set._acl['Kevin'].r).toBeUndefined(); expect(set._rperm).toEqual(input._rperm); expect(set._wperm).toEqual(input._wperm); done(); }); it('untransforms from _rperm and _wperm to ACL', done => { const input = { _rperm: ['*'], _wperm: ['Kevin'], }; const output = transform.mongoObjectToParseObject(null, input, { fields: {}, }); expect(output._rperm).toEqual(['*']); expect(output._wperm).toEqual(['Kevin']); expect(output.ACL).toBeUndefined(); done(); }); it('untransforms mongodb number types', done => { const input = { long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER), double: new mongodb.Double(Number.MAX_VALUE), }; const output = transform.mongoObjectToParseObject(null, input, { fields: { long: { type: 'Number' }, double: { type: 'Number' }, }, }); expect(output.long).toBe(Number.MAX_SAFE_INTEGER); expect(output.double).toBe(Number.MAX_VALUE); done(); }); it('Date object where iso attribute is of type Date', done => { const input = { ts: { __type: 'Date', iso: new Date('2017-01-18T00:00:00.000Z') }, }; const output = transform.mongoObjectToParseObject(null, input, { fields: { ts: { type: 'Date' }, }, }); expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z'); done(); }); it('Date object where iso attribute is of type String', done => { const input = { ts: { __type: 'Date', iso: '2017-01-18T00:00:00.000Z' }, }; const output = transform.mongoObjectToParseObject(null, input, { fields: { ts: { type: 'Date' }, }, }); expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z'); done(); }); it('object with undefined nested values', () => { const input = { _id: 'vQHyinCW1l', urls: { firstUrl: 'https://', secondUrl: undefined }, }; const output = transform.mongoObjectToParseObject(null, input, { fields: { urls: { type: 'Object' }, }, }); expect(output.urls).toEqual({ firstUrl: 'https://', secondUrl: undefined, }); }); it('undefined objects', () => { const input = { _id: 'vQHyinCW1l', urls: undefined, }; const output = transform.mongoObjectToParseObject(null, input, { fields: { urls: { type: 'Object' }, }, }); expect(output.urls).toBeUndefined(); }); it('$regex in $all list', done => { const input = { arrayField: { $all: [{ $regex: '^\\Qone\\E' }, { $regex: '^\\Qtwo\\E' }, { $regex: '^\\Qthree\\E' }], }, }; const outputValue = { arrayField: { $all: [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/] }, }; const output = transform.transformWhere(null, input); jequal(outputValue.arrayField, output.arrayField); done(); }); it('$regex in $all list must be { $regex: "string" }', done => { const input = { arrayField: { $all: [{ $regex: 1 }] }, }; expect(() => { transform.transformWhere(null, input); }).toThrow(); done(); }); it('all values in $all must be $regex (start with string) or non $regex (start with string)', done => { const input = { arrayField: { $all: [{ $regex: '^\\Qone\\E' }, { $unknown: '^\\Qtwo\\E' }], }, }; expect(() => { transform.transformWhere(null, input); }).toThrow(); done(); }); it('ignores User authData field in DB so it can be synthesized in code', done => { const input = { _id: '123', _auth_data_acme: { id: 'abc' }, authData: null, }; const output = transform.mongoObjectToParseObject('_User', input, { fields: {}, }); expect(output.authData.acme.id).toBe('abc'); done(); }); it('can set authData when not User class', done => { const input = { _id: '123', authData: 'random', }; const output = transform.mongoObjectToParseObject('TestObject', input, { fields: {}, }); expect(output.authData).toBe('random'); done(); }); it('should only transform authData.provider.id for _User class', () => { // Test that for _User class, authData.facebook.id is transformed const userInput = { 'authData.facebook.id': '10000000000000001', }; const userOutput = transform.transformWhere('_User', userInput, { fields: {} }); expect(userOutput['_auth_data_facebook.id']).toBe('10000000000000001'); // Test that for non-User classes, authData.facebook.id is NOT transformed const customInput = { 'authData.facebook.id': '10000000000000001', }; const customOutput = transform.transformWhere('SpamAlerts', customInput, { fields: {} }); expect(customOutput['authData.facebook.id']).toBe('10000000000000001'); expect(customOutput['_auth_data_facebook.id']).toBeUndefined(); }); }); it('cannot have a custom field name beginning with underscore', done => { const input = { _id: '123', _thisFieldNameIs: 'invalid', }; try { transform.mongoObjectToParseObject('TestObject', input, { fields: {}, }); } catch (e) { expect(e).toBeDefined(); } done(); }); describe('transformUpdate', () => { it('removes Relation types', done => { const input = { aRelation: { __type: 'Relation', className: 'Stuff' }, }; const output = transform.transformUpdate(null, input, { fields: { aRelation: { __type: 'Relation', className: 'Stuff' }, }, }); expect(output).toEqual({}); done(); }); }); describe('transformConstraint', () => { describe('$relativeTime', () => { it('should error on $eq, $ne, and $exists', () => { expect(() => { transform.transformConstraint({ $eq: { ttl: { $relativeTime: '12 days ago', }, }, }); }).toThrow(); expect(() => { transform.transformConstraint({ $ne: { ttl: { $relativeTime: '12 days ago', }, }, }); }).toThrow(); expect(() => { transform.transformConstraint({ $exists: { $relativeTime: '12 days ago', }, }); }).toThrow(); }); }); }); describe('relativeTimeToDate', () => { const now = new Date('2017-09-26T13:28:16.617Z'); describe('In the future', () => { it('should parse valid natural time', () => { const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds'; const { result, status, info } = Utils.relativeTimeToDate(text, now); expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z'); expect(status).toBe('success'); expect(info).toBe('future'); }); }); describe('In the past', () => { it('should parse valid natural time', () => { const text = '2 days 12 hours 1 minute 12 seconds ago'; const { result, status, info } = Utils.relativeTimeToDate(text, now); expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z'); expect(status).toBe('success'); expect(info).toBe('past'); }); }); describe('From now', () => { it('should equal current time', () => { const text = 'now'; const { result, status, info } = Utils.relativeTimeToDate(text, now); expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z'); expect(status).toBe('success'); expect(info).toBe('present'); }); }); describe('Error cases', () => { it('should error if string is completely gibberish', () => { expect(Utils.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({ status: 'error', info: "Time should either start with 'in' or end with 'ago'", }); }); it('should error if string contains neither `ago` nor `in`', () => { expect(Utils.relativeTimeToDate('12 hours 1 minute')).toEqual({ status: 'error', info: "Time should either start with 'in' or end with 'ago'", }); }); it('should error if there are missing units or numbers', () => { expect(Utils.relativeTimeToDate('in 12 hours 1')).toEqual({ status: 'error', info: 'Invalid time string. Dangling unit or number.', }); expect(Utils.relativeTimeToDate('12 hours minute ago')).toEqual({ status: 'error', info: 'Invalid time string. Dangling unit or number.', }); }); it('should error on floating point numbers', () => { expect(Utils.relativeTimeToDate('in 12.3 hours')).toEqual({ status: 'error', info: "'12.3' is not an integer.", }); }); it('should error if numbers are invalid', () => { expect(Utils.relativeTimeToDate('12 hours 123a minute ago')).toEqual({ status: 'error', info: "'123a' is not an integer.", }); }); it('should error on invalid interval units', () => { expect(Utils.relativeTimeToDate('4 score 7 years ago')).toEqual({ status: 'error', info: "Invalid interval: 'score'", }); }); it("should error when string contains 'ago' and 'in'", () => { expect(Utils.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({ status: 'error', info: "Time cannot have both 'in' and 'ago'", }); }); }); });