'use strict';
const Config = require('../lib/Config');

describe('Pointer Permissions', () => {
  beforeEach(() => {
    Config.get(Parse.applicationId).schemaCache.clear();
  });

  describe('using single user-pointers', () => {
    it('should work with find', done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');

      Parse.Object.saveAll([user, user2])
        .then(() => {
          obj.set('owner', user);
          obj2.set('owner', user2);
          return Parse.Object.saveAll([obj, obj2]);
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            return schema.updateClass('AnObject', {}, { readUserFields: ['owner'] });
          });
        })
        .then(() => {
          return Parse.User.logIn('user1', 'password');
        })
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.find();
        })
        .then(res => {
          expect(res.length).toBe(1);
          expect(res[0].id).toBe(obj.id);
          done();
        })
        .catch(error => {
          fail(JSON.stringify(error));
          done();
        });
    });

    it('should work with write', done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');

      Parse.Object.saveAll([user, user2])
        .then(() => {
          obj.set('owner', user);
          obj.set('reader', user2);
          obj2.set('owner', user2);
          obj2.set('reader', user);
          return Parse.Object.saveAll([obj, obj2]);
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            return schema.updateClass(
              'AnObject',
              {},
              {
                writeUserFields: ['owner'],
                readUserFields: ['reader', 'owner'],
              }
            );
          });
        })
        .then(() => {
          return Parse.User.logIn('user1', 'password');
        })
        .then(() => {
          obj2.set('hello', 'world');
          return obj2.save();
        })
        .then(
          () => {
            fail('User should not be able to update obj2');
          },
          err => {
            // User 1 should not be able to update obj2
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            return Promise.resolve();
          }
        )
        .then(() => {
          obj.set('hello', 'world');
          return obj.save();
        })
        .then(
          () => {
            return Parse.User.logIn('user2', 'password');
          },
          () => {
            fail('User should be able to update');
            return Promise.resolve();
          }
        )
        .then(
          () => {
            const q = new Parse.Query('AnObject');
            return q.find();
          },
          () => {
            fail('should login with user 2');
          }
        )
        .then(
          res => {
            expect(res.length).toBe(2);
            res.forEach(result => {
              if (result.id == obj.id) {
                expect(result.get('hello')).toBe('world');
              } else {
                expect(result.id).toBe(obj2.id);
              }
            });
            done();
          },
          () => {
            fail('failed');
            done();
          }
        );
    });

    it('should let a proper user find', done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');
      user
        .signUp()
        .then(() => {
          return user2.signUp();
        })
        .then(() => {
          Parse.User.logOut();
        })
        .then(() => {
          obj.set('owner', user);
          return Parse.Object.saveAll([obj, obj2]);
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            return schema.updateClass(
              'AnObject',
              {},
              { find: {}, get: {}, readUserFields: ['owner'] }
            );
          });
        })
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.find();
        })
        .then(res => {
          expect(res.length).toBe(0);
        })
        .then(() => {
          return Parse.User.logIn('user2', 'password');
        })
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.find();
        })
        .then(res => {
          expect(res.length).toBe(0);
          const q = new Parse.Query('AnObject');
          return q.get(obj.id);
        })
        .then(
          () => {
            fail('User 2 should not get the obj1 object');
          },
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            expect(err.message).toBe('Object not found.');
            return Promise.resolve();
          }
        )
        .then(() => {
          return Parse.User.logIn('user1', 'password');
        })
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.find();
        })
        .then(res => {
          expect(res.length).toBe(1);
          done();
        })
        .catch(err => {
          jfail(err);
          fail('should not fail');
          done();
        });
    });

    it_id('f38c35e7-d804-4d32-986d-2579e25d2461')(it)('should query on pointer permission enabled column', done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');
      user
        .signUp()
        .then(() => {
          return user2.signUp();
        })
        .then(() => {
          Parse.User.logOut();
        })
        .then(() => {
          obj.set('owner', user);
          return Parse.Object.saveAll([obj, obj2]);
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            return schema.updateClass(
              'AnObject',
              {},
              { find: {}, get: {}, readUserFields: ['owner'] }
            );
          });
        })
        .then(() => {
          return Parse.User.logIn('user1', 'password');
        })
        .then(() => {
          const q = new Parse.Query('AnObject');
          q.equalTo('owner', user2);
          return q.find();
        })
        .then(res => {
          expect(res.length).toBe(0);
          done();
        })
        .catch(err => {
          jfail(err);
          fail('should not fail');
          done();
        });
    });

    it('should not allow creating objects', done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      user
        .save()
        .then(() => {
          return config.database.loadSchema().then(schema => {
            return schema.addClassIfNotExists(
              'AnObject',
              { owner: { type: 'Pointer', targetClass: '_User' } },
              {
                create: {},
                writeUserFields: ['owner'],
                readUserFields: ['owner'],
              }
            );
          });
        })
        .then(() => {
          return Parse.User.logIn('user1', 'password');
        })
        .then(() => {
          obj.set('owner', user);
          return obj.save();
        })
        .then(
          () => {
            fail('should not succeed');
            done();
          },
          err => {
            expect(err.code).toBe(119);
            done();
          }
        );
    });

    it('should handle multiple writeUserFields', done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      Parse.Object.saveAll([user, user2])
        .then(() => {
          obj.set('owner', user);
          obj.set('otherOwner', user2);
          return obj.save();
        })
        .then(() => config.database.loadSchema())
        .then(schema =>
          schema.updateClass(
            'AnObject',
            {},
            { find: { '*': true }, writeUserFields: ['owner', 'otherOwner'] }
          )
        )
        .then(() => Parse.User.logIn('user1', 'password'))
        .then(() => obj.save({ hello: 'fromUser1' }))
        .then(() => Parse.User.logIn('user2', 'password'))
        .then(() => obj.save({ hello: 'fromUser2' }))
        .then(() => Parse.User.logOut())
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.first();
        })
        .then(result => {
          expect(result.get('hello')).toBe('fromUser2');
          done();
        })
        .catch(() => {
          fail('should not fail');
          done();
        });
    });

    it('should prevent creating pointer permission on missing field', done => {
      const config = Config.get(Parse.applicationId);
      config.database
        .loadSchema()
        .then(schema => {
          return schema.addClassIfNotExists(
            'AnObject',
            {},
            {
              create: {},
              writeUserFields: ['owner'],
              readUserFields: ['owner'],
            }
          );
        })
        .then(() => {
          fail('should not succeed');
        })
        .catch(err => {
          expect(err.code).toBe(107);
          expect(err.message).toBe(
            "'owner' is not a valid column for class level pointer permissions writeUserFields"
          );
          done();
        });
    });

    it('should prevent creating pointer permission on bad field (of wrong type)', done => {
      const config = Config.get(Parse.applicationId);
      config.database
        .loadSchema()
        .then(schema => {
          return schema.addClassIfNotExists(
            'AnObject',
            { owner: { type: 'String' } },
            {
              create: {},
              writeUserFields: ['owner'],
              readUserFields: ['owner'],
            }
          );
        })
        .then(() => {
          fail('should not succeed');
        })
        .catch(err => {
          expect(err.code).toBe(107);
          expect(err.message).toBe(
            "'owner' is not a valid column for class level pointer permissions writeUserFields"
          );
          done();
        });
    });

    it('should prevent creating pointer permission on bad field (non-user pointer)', done => {
      const config = Config.get(Parse.applicationId);
      config.database
        .loadSchema()
        .then(schema => {
          return schema.addClassIfNotExists(
            'AnObject',
            { owner: { type: 'Pointer', targetClass: '_Session' } },
            {
              create: {},
              writeUserFields: ['owner'],
              readUserFields: ['owner'],
            }
          );
        })
        .then(() => {
          fail('should not succeed');
        })
        .catch(err => {
          expect(err.code).toBe(107);
          expect(err.message).toBe(
            "'owner' is not a valid column for class level pointer permissions writeUserFields"
          );
          done();
        });
    });

    it('should prevent creating pointer permission on bad field (non-existing)', done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('owner', 'value');
      object
        .save()
        .then(() => {
          return config.database.loadSchema();
        })
        .then(schema => {
          return schema.updateClass(
            'AnObject',
            {},
            {
              create: {},
              writeUserFields: ['owner'],
              readUserFields: ['owner'],
            }
          );
        })
        .then(() => {
          fail('should not succeed');
        })
        .catch(err => {
          expect(err.code).toBe(107);
          expect(err.message).toBe(
            "'owner' is not a valid column for class level pointer permissions writeUserFields"
          );
          done();
        });
    });

    it('tests CLP / Pointer Perms / ACL write (PP Locked)', done => {
      /*
        tests:
        CLP: update closed ({})
        PointerPerm: "owner"
        ACL: logged in user has access

        The owner is another user than the ACL
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      Parse.Object.saveAll([user, user2])
        .then(() => {
          const ACL = new Parse.ACL();
          ACL.setReadAccess(user, true);
          ACL.setWriteAccess(user, true);
          obj.setACL(ACL);
          obj.set('owner', user2);
          return obj.save();
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass('AnObject', {}, { update: {}, writeUserFields: ['owner'] });
          });
        })
        .then(() => {
          return Parse.User.logIn('user1', 'password');
        })
        .then(() => {
          // user1 has ACL read/write but should be blocked by PP
          return obj.save({ key: 'value' });
        })
        .then(
          () => {
            fail('Should not succeed saving');
            done();
          },
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            done();
          }
        );
    });

    it('tests CLP / Pointer Perms / ACL write (ACL Locked)', done => {
      /*
        tests:
        CLP: update closed ({})
        PointerPerm: "owner"
        ACL: logged in user has access
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      Parse.Object.saveAll([user, user2])
        .then(() => {
          const ACL = new Parse.ACL();
          ACL.setReadAccess(user, true);
          ACL.setWriteAccess(user, true);
          obj.setACL(ACL);
          obj.set('owner', user2);
          return obj.save();
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass('AnObject', {}, { update: {}, writeUserFields: ['owner'] });
          });
        })
        .then(() => {
          return Parse.User.logIn('user2', 'password');
        })
        .then(() => {
          // user1 has ACL read/write but should be blocked by ACL
          return obj.save({ key: 'value' });
        })
        .then(
          () => {
            fail('Should not succeed saving');
            done();
          },
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            done();
          }
        );
    });

    it('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', done => {
      /*
        tests:
        CLP: update closed ({})
        PointerPerm: "owner"
        ACL: logged in user has access
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      Parse.Object.saveAll([user, user2])
        .then(() => {
          const ACL = new Parse.ACL();
          ACL.setWriteAccess(user, true);
          ACL.setWriteAccess(user2, true);
          obj.setACL(ACL);
          obj.set('owner', user2);
          return obj.save();
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass('AnObject', {}, { update: {}, writeUserFields: ['owner'] });
          });
        })
        .then(() => {
          return Parse.User.logIn('user2', 'password');
        })
        .then(() => {
          // user1 has ACL read/write but should be blocked by ACL
          return obj.save({ key: 'value' });
        })
        .then(
          objAgain => {
            expect(objAgain.get('key')).toBe('value');
            done();
          },
          () => {
            fail('Should not fail saving');
            done();
          }
        );
    });

    it('tests CLP / Pointer Perms / ACL read (PP locked)', done => {
      /*
        tests:
        CLP: find/get open ({})
        PointerPerm: "owner" : read
        ACL: logged in user has access

        The owner is another user than the ACL
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      Parse.Object.saveAll([user, user2])
        .then(() => {
          const ACL = new Parse.ACL();
          ACL.setReadAccess(user, true);
          ACL.setWriteAccess(user, true);
          obj.setACL(ACL);
          obj.set('owner', user2);
          return obj.save();
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass(
              'AnObject',
              {},
              { find: {}, get: {}, readUserFields: ['owner'] }
            );
          });
        })
        .then(() => {
          return Parse.User.logIn('user1', 'password');
        })
        .then(() => {
          // user1 has ACL read/write but should be block
          return obj.fetch();
        })
        .then(
          () => {
            fail('Should not succeed saving');
            done();
          },
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            done();
          }
        );
    });

    it('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', done => {
      /*
        tests:
        CLP: find/get open ({"*": true})
        PointerPerm: "owner" : read
        ACL: logged in user has access
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      Parse.Object.saveAll([user, user2])
        .then(() => {
          const ACL = new Parse.ACL();
          ACL.setReadAccess(user, true);
          ACL.setWriteAccess(user, true);
          ACL.setReadAccess(user2, true);
          ACL.setWriteAccess(user2, true);
          obj.setACL(ACL);
          obj.set('owner', user2);
          return obj.save();
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass(
              'AnObject',
              {},
              {
                find: { '*': true },
                get: { '*': true },
                readUserFields: ['owner'],
              }
            );
          });
        })
        .then(() => {
          return Parse.User.logIn('user2', 'password');
        })
        .then(() => {
          // user1 has ACL read/write but should be block
          return obj.fetch();
        })
        .then(
          objAgain => {
            expect(objAgain.id).toBe(obj.id);
            done();
          },
          () => {
            fail('Should not fail fetching');
            done();
          }
        );
    });

    it('tests CLP / Pointer Perms / ACL read (ACL locked)', done => {
      /*
        tests:
        CLP: find/get open ({"*": true})
        PointerPerm: "owner" : read // proper owner
        ACL: logged in user has not access
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      Parse.Object.saveAll([user, user2])
        .then(() => {
          const ACL = new Parse.ACL();
          ACL.setReadAccess(user, true);
          ACL.setWriteAccess(user, true);
          obj.setACL(ACL);
          obj.set('owner', user2);
          return obj.save();
        })
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass(
              'AnObject',
              {},
              {
                find: { '*': true },
                get: { '*': true },
                readUserFields: ['owner'],
              }
            );
          });
        })
        .then(() => {
          return Parse.User.logIn('user2', 'password');
        })
        .then(() => {
          // user2 has ACL read/write but should be block by ACL
          return obj.fetch();
        })
        .then(
          () => {
            fail('Should not succeed saving');
            done();
          },
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            done();
          }
        );
    });

    it('should let master key find objects', done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('hello', 'world');
      return object
        .save()
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass(
              'AnObject',
              { owner: { type: 'Pointer', targetClass: '_User' } },
              { find: {}, get: {}, readUserFields: ['owner'] }
            );
          });
        })
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.find();
        })
        .then(
          () => {},
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            return Promise.resolve();
          }
        )
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.find({ useMasterKey: true });
        })
        .then(
          objects => {
            expect(objects.length).toBe(1);
            done();
          },
          () => {
            fail('master key should find the object');
            done();
          }
        );
    });

    it('should let master key get objects', done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('hello', 'world');
      return object
        .save()
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass(
              'AnObject',
              { owner: { type: 'Pointer', targetClass: '_User' } },
              { find: {}, get: {}, readUserFields: ['owner'] }
            );
          });
        })
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.get(object.id);
        })
        .then(
          () => {},
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            return Promise.resolve();
          }
        )
        .then(() => {
          const q = new Parse.Query('AnObject');
          return q.get(object.id, { useMasterKey: true });
        })
        .then(
          objectAgain => {
            expect(objectAgain).not.toBeUndefined();
            expect(objectAgain.id).toBe(object.id);
            done();
          },
          () => {
            fail('master key should find the object');
            done();
          }
        );
    });

    it('should let master key update objects', done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('hello', 'world');
      return object
        .save()
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass(
              'AnObject',
              { owner: { type: 'Pointer', targetClass: '_User' } },
              { update: {}, writeUserFields: ['owner'] }
            );
          });
        })
        .then(() => {
          return object.save({ hello: 'bar' });
        })
        .then(
          () => {},
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            return Promise.resolve();
          }
        )
        .then(() => {
          return object.save({ hello: 'baz' }, { useMasterKey: true });
        })
        .then(
          objectAgain => {
            expect(objectAgain.get('hello')).toBe('baz');
            done();
          },
          () => {
            fail('master key should save the object');
            done();
          }
        );
    });

    it('should let master key delete objects', done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('hello', 'world');
      return object
        .save()
        .then(() => {
          return config.database.loadSchema().then(schema => {
            // Lock the update, and let only owner write
            return schema.updateClass(
              'AnObject',
              { owner: { type: 'Pointer', targetClass: '_User' } },
              { delete: {}, writeUserFields: ['owner'] }
            );
          });
        })
        .then(() => {
          return object.destroy();
        })
        .then(
          () => {
            fail();
          },
          err => {
            expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
            return Promise.resolve();
          }
        )
        .then(() => {
          return object.destroy({ useMasterKey: true });
        })
        .then(
          () => {
            done();
          },
          () => {
            fail('master key should destroy the object');
            done();
          }
        );
    });

    it('should fail with invalid pointer perms (not array)', done => {
      const config = Config.get(Parse.applicationId);
      config.database
        .loadSchema()
        .then(schema => {
          // Lock the update, and let only owner write
          return schema.addClassIfNotExists(
            'AnObject',
            { owner: { type: 'Pointer', targetClass: '_User' } },
            { delete: {}, writeUserFields: 'owner' }
          );
        })
        .catch(err => {
          expect(err.code).toBe(Parse.Error.INVALID_JSON);
          done();
        });
    });

    it('should fail with invalid pointer perms (non-existing field)', done => {
      const config = Config.get(Parse.applicationId);
      config.database
        .loadSchema()
        .then(schema => {
          // Lock the update, and let only owner write
          return schema.addClassIfNotExists(
            'AnObject',
            { owner: { type: 'Pointer', targetClass: '_User' } },
            { delete: {}, writeUserFields: ['owner', 'invalid'] }
          );
        })
        .catch(err => {
          expect(err.code).toBe(Parse.Error.INVALID_JSON);
          done();
        });
    });
  });

  describe('using arrays of user-pointers', () => {
    it('should work with find', async done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2]);

      obj.set('owners', [user]);
      obj2.set('owners', [user2]);
      await Parse.Object.saveAll([obj, obj2]);

      const schema = await config.database.loadSchema();
      await schema.updateClass('AnObject', {}, { readUserFields: ['owners'] });

      await Parse.User.logIn('user1', 'password');

      try {
        const q = new Parse.Query('AnObject');
        const res = await q.find();
        expect(res.length).toBe(1);
        expect(res[0].id).toBe(obj.id);
        done();
      } catch (err) {
        done.fail(JSON.stringify(err));
      }
    });

    it_id('1bbb9ed6-5558-4ce5-a238-b1a2015d273f')(it)('should work with write', async done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2]);

      obj.set('owner', user);
      obj.set('readers', [user2]);
      obj2.set('owner', user2);
      obj2.set('readers', [user]);
      await Parse.Object.saveAll([obj, obj2]);

      const schema = await config.database.loadSchema();
      await schema.updateClass(
        'AnObject',
        {},
        {
          writeUserFields: ['owner'],
          readUserFields: ['readers', 'owner'],
        }
      );

      await Parse.User.logIn('user1', 'password');

      obj2.set('hello', 'world');
      try {
        await obj2.save();
        done.fail('User should not be able to update obj2');
      } catch (err) {
        // User 1 should not be able to update obj2
        expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
      }

      obj.set('hello', 'world');
      try {
        await obj.save();
      } catch (err) {
        done.fail('User should be able to update');
      }

      await Parse.User.logIn('user2', 'password');

      try {
        const q = new Parse.Query('AnObject');
        const res = await q.find();
        expect(res.length).toBe(2);
        res.forEach(result => {
          if (result.id == obj.id) {
            expect(result.get('hello')).toBe('world');
          } else {
            expect(result.id).toBe(obj2.id);
          }
        });
        done();
      } catch (err) {
        done.fail('failed');
      }
    });

    it('should let a proper user find', async done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');

      await user.signUp();
      await user2.signUp();
      await user3.signUp();
      await Parse.User.logOut();

      obj.set('owners', [user, user2]);
      await Parse.Object.saveAll([obj, obj2]);

      const schema = await config.database.loadSchema();
      await schema.updateClass('AnObject', {}, { find: {}, get: {}, readUserFields: ['owners'] });

      let q = new Parse.Query('AnObject');
      let result = await q.find();
      expect(result.length).toBe(0);

      Parse.User.logIn('user3', 'password');
      q = new Parse.Query('AnObject');
      result = await q.find();

      expect(result.length).toBe(0);
      q = new Parse.Query('AnObject');

      try {
        await q.get(obj.id);
        done.fail('User 3 should not get the obj1 object');
      } catch (err) {
        expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
        expect(err.message).toBe('Object not found.');
      }

      for (const owner of ['user1', 'user2']) {
        await Parse.User.logIn(owner, 'password');
        try {
          const q = new Parse.Query('AnObject');
          result = await q.find();
          expect(result.length).toBe(1);
        } catch (err) {
          done.fail('should not fail');
        }
      }
      done();
    });

    it_id('8a7d188c-b75c-4eac-90b6-9b0b11f873ae')(it)('should query on pointer permission enabled column', async done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');

      await user.signUp();
      await user2.signUp();
      await user3.signUp();
      await Parse.User.logOut();

      obj.set('owners', [user, user2]);
      await Parse.Object.saveAll([obj, obj2]);

      const schema = await config.database.loadSchema();
      await schema.updateClass('AnObject', {}, { find: {}, get: {}, readUserFields: ['owners'] });

      for (const owner of ['user1', 'user2']) {
        await Parse.User.logIn(owner, 'password');
        try {
          const q = new Parse.Query('AnObject');
          q.equalTo('owners', user3);
          const result = await q.find();
          expect(result.length).toBe(0);
        } catch (err) {
          done.fail('should not fail');
        }
      }
      done();
    });

    it('should not query using arrays on pointer permission enabled column', async done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      const obj2 = new Parse.Object('AnObject');

      await user.signUp();
      await user2.signUp();
      await user3.signUp();
      await Parse.User.logOut();

      obj.set('owners', [user, user2]);
      await Parse.Object.saveAll([obj, obj2]);

      const schema = await config.database.loadSchema();
      await schema.updateClass('AnObject', {}, { find: {}, get: {}, readUserFields: ['owners'] });

      for (const owner of ['user1', 'user2']) {
        try {
          await Parse.User.logIn(owner, 'password');
          // Since querying for arrays is not supported this should throw an error
          const q = new Parse.Query('AnObject');
          q.equalTo('owners', [user3]);
          await q.find();
          done.fail('should fail');
          // eslint-disable-next-line no-empty
        } catch (error) {}
      }
      done();
    });

    it('should not allow creating objects', async done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      await Parse.Object.saveAll([user, user2]);

      const schema = await config.database.loadSchema();
      await schema.addClassIfNotExists(
        'AnObject',
        { owners: { type: 'Array' } },
        {
          create: {},
          writeUserFields: ['owners'],
          readUserFields: ['owners'],
        }
      );

      for (const owner of ['user1', 'user2']) {
        await Parse.User.logIn(owner, 'password');
        try {
          obj.set('owners', [user, user2]);
          await obj.save();
          done.fail('should not succeed');
        } catch (err) {
          expect(err.code).toBe(119);
        }
      }
      done();
    });

    it('should handle multiple writeUserFields', async done => {
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2]);
      obj.set('owners', [user]);
      obj.set('otherOwners', [user2]);
      await obj.save();

      const schema = await config.database.loadSchema();
      await schema.updateClass(
        'AnObject',
        {},
        { find: { '*': true }, writeUserFields: ['owners', 'otherOwners'] }
      );

      await Parse.User.logIn('user1', 'password');
      await obj.save({ hello: 'fromUser1' });
      await Parse.User.logIn('user2', 'password');
      await obj.save({ hello: 'fromUser2' });
      await Parse.User.logOut();

      try {
        const q = new Parse.Query('AnObject');
        const result = await q.first();
        expect(result.get('hello')).toBe('fromUser2');
        done();
      } catch (err) {
        done.fail('should not fail');
      }
    });

    it('should prevent creating pointer permission on missing field', async done => {
      const config = Config.get(Parse.applicationId);
      const schema = await config.database.loadSchema();
      try {
        await schema.addClassIfNotExists(
          'AnObject',
          {},
          {
            create: {},
            writeUserFields: ['owners'],
            readUserFields: ['owners'],
          }
        );
        done.fail('should not succeed');
      } catch (err) {
        expect(err.code).toBe(107);
        expect(err.message).toBe(
          "'owners' is not a valid column for class level pointer permissions writeUserFields"
        );
        done();
      }
    });

    it('should prevent creating pointer permission on bad field (of wrong type)', async done => {
      const config = Config.get(Parse.applicationId);
      const schema = await config.database.loadSchema();
      try {
        await schema.addClassIfNotExists(
          'AnObject',
          { owners: { type: 'String' } },
          {
            create: {},
            writeUserFields: ['owners'],
            readUserFields: ['owners'],
          }
        );
        done.fail('should not succeed');
      } catch (err) {
        expect(err.code).toBe(107);
        expect(err.message).toBe(
          "'owners' is not a valid column for class level pointer permissions writeUserFields"
        );
        done();
      }
    });

    it('should prevent creating pointer permission on bad field (non-existing)', async done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('owners', 'value');
      await object.save();

      const schema = await config.database.loadSchema();
      try {
        await schema.updateClass(
          'AnObject',
          {},
          {
            create: {},
            writeUserFields: ['owners'],
            readUserFields: ['owners'],
          }
        );
        done.fail('should not succeed');
      } catch (err) {
        expect(err.code).toBe(107);
        expect(err.message).toBe(
          "'owners' is not a valid column for class level pointer permissions writeUserFields"
        );
        done();
      }
    });

    it('should work with arrays containing valid & invalid elements', async done => {
      /* Since there is no way to check the validity of objects in arrays before querying invalid
         elements in arrays should be ignored. */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2]);

      obj.set('owners', [user, '', -1, true, [], { invalid: -1 }]);
      await Parse.Object.saveAll([obj]);

      const schema = await config.database.loadSchema();
      await schema.updateClass('AnObject', {}, { readUserFields: ['owners'] });

      await Parse.User.logIn('user1', 'password');

      try {
        const q = new Parse.Query('AnObject');
        const res = await q.find();
        expect(res.length).toBe(1);
        expect(res[0].id).toBe(obj.id);
      } catch (err) {
        done.fail(JSON.stringify(err));
      }

      await Parse.User.logOut();
      await Parse.User.logIn('user2', 'password');

      try {
        const q = new Parse.Query('AnObject');
        const res = await q.find();
        expect(res.length).toBe(0);
        done();
      } catch (err) {
        done.fail(JSON.stringify(err));
      }
    });

    it('tests CLP / Pointer Perms / ACL write (PP Locked)', async done => {
      /*
        tests:
        CLP: update closed ({})
        PointerPerm: "owners"
        ACL: logged in user has access

        The owner is another user than the ACL
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2, user3]);

      const ACL = new Parse.ACL();
      ACL.setReadAccess(user, true);
      ACL.setWriteAccess(user, true);
      obj.setACL(ACL);
      obj.set('owners', [user2, user3]);
      await obj.save();

      const schema = await config.database.loadSchema();
      // Lock the update, and let only owners write
      await schema.updateClass('AnObject', {}, { update: {}, writeUserFields: ['owners'] });

      await Parse.User.logIn('user1', 'password');
      try {
        // user1 has ACL read/write but should be blocked by PP
        await obj.save({ key: 'value' });
        done.fail('Should not succeed saving');
      } catch (err) {
        expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
        done();
      }
    });

    it('tests CLP / Pointer Perms / ACL write (ACL Locked)', async done => {
      /*
        tests:
        CLP: update closed ({})
        PointerPerm: "owners"
        ACL: logged in user has access
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2, user3]);

      const ACL = new Parse.ACL();
      ACL.setReadAccess(user, true);
      ACL.setWriteAccess(user, true);
      obj.setACL(ACL);
      obj.set('owners', [user2, user3]);
      await obj.save();

      const schema = await config.database.loadSchema();
      // Lock the update, and let only owners write
      await schema.updateClass('AnObject', {}, { update: {}, writeUserFields: ['owners'] });

      for (const owner of ['user2', 'user3']) {
        await Parse.User.logIn(owner, 'password');
        try {
          await obj.save({ key: 'value' });
          done.fail('Should not succeed saving');
        } catch (err) {
          expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
        }
      }
      done();
    });

    it('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', async done => {
      /*
        tests:
        CLP: update closed ({})
        PointerPerm: "owners"
        ACL: logged in user has access
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2, user3]);
      const ACL = new Parse.ACL();
      ACL.setWriteAccess(user, true);
      ACL.setWriteAccess(user2, true);
      ACL.setWriteAccess(user3, true);
      obj.setACL(ACL);
      obj.set('owners', [user2, user3]);
      await obj.save();

      const schema = await config.database.loadSchema();
      // Lock the update, and let only owners write
      await schema.updateClass('AnObject', {}, { update: {}, writeUserFields: ['owners'] });

      for (const owner of ['user2', 'user3']) {
        await Parse.User.logIn(owner, 'password');
        try {
          const objectAgain = await obj.save({ key: 'value' });
          expect(objectAgain.get('key')).toBe('value');
        } catch (err) {
          done.fail('Should not fail saving');
        }
      }
      done();
    });

    it('tests CLP / Pointer Perms / ACL read (PP locked)', async done => {
      /*
        tests:
        CLP: find/get open ({})
        PointerPerm: "owners" : read
        ACL: logged in user has access

        The owner is another user than the ACL
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2, user3]);

      const ACL = new Parse.ACL();
      ACL.setReadAccess(user, true);
      ACL.setWriteAccess(user, true);
      obj.setACL(ACL);
      obj.set('owners', [user2, user3]);
      await obj.save();

      const schema = await config.database.loadSchema();
      // Lock reading, and let only owners read
      await schema.updateClass('AnObject', {}, { find: {}, get: {}, readUserFields: ['owners'] });

      await Parse.User.logIn('user1', 'password');
      try {
        // user1 has ACL read/write but should be blocked
        await obj.fetch();
        done.fail('Should not succeed fetching');
      } catch (err) {
        expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
        done();
      }
      done();
    });

    it('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', async done => {
      /*
        tests:
        CLP: find/get open ({"*": true})
        PointerPerm: "owners" : read
        ACL: logged in user has access
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');

      await Parse.Object.saveAll([user, user2, user3]);

      const ACL = new Parse.ACL();
      ACL.setReadAccess(user, true);
      ACL.setWriteAccess(user, true);
      ACL.setReadAccess(user2, true);
      ACL.setWriteAccess(user2, true);
      ACL.setReadAccess(user3, true);
      ACL.setWriteAccess(user3, true);
      obj.setACL(ACL);
      obj.set('owners', [user2, user3]);
      await obj.save();

      const schema = await config.database.loadSchema();
      // Allow public and owners read
      await schema.updateClass(
        'AnObject',
        {},
        {
          find: { '*': true },
          get: { '*': true },
          readUserFields: ['owners'],
        }
      );

      for (const owner of ['user2', 'user3']) {
        await Parse.User.logIn(owner, 'password');
        try {
          const objectAgain = await obj.fetch();
          expect(objectAgain.id).toBe(obj.id);
        } catch (err) {
          done.fail('Should not fail fetching');
        }
      }
      done();
    });

    it('tests CLP / Pointer Perms / ACL read (ACL locked)', async done => {
      /*
        tests:
        CLP: find/get open ({"*": true})
        PointerPerm: "owners" : read // proper owner
        ACL: logged in user has not access
       */
      const config = Config.get(Parse.applicationId);
      const user = new Parse.User();
      const user2 = new Parse.User();
      const user3 = new Parse.User();
      user.set({
        username: 'user1',
        password: 'password',
      });
      user2.set({
        username: 'user2',
        password: 'password',
      });
      user3.set({
        username: 'user3',
        password: 'password',
      });
      const obj = new Parse.Object('AnObject');
      await Parse.Object.saveAll([user, user2, user3]);

      const ACL = new Parse.ACL();
      ACL.setReadAccess(user, true);
      ACL.setWriteAccess(user, true);
      obj.setACL(ACL);
      obj.set('owners', [user2, user3]);
      await obj.save();

      const schema = await config.database.loadSchema();
      // Allow public and owners read
      await schema.updateClass(
        'AnObject',
        {},
        {
          find: { '*': true },
          get: { '*': true },
          readUserFields: ['owners'],
        }
      );

      for (const owner of ['user2', 'user3']) {
        await Parse.User.logIn(owner, 'password');
        try {
          await obj.fetch();
          done.fail('Should not succeed fetching');
        } catch (err) {
          expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
        }
      }
      done();
    });

    it('should let master key find objects', async done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('hello', 'world');
      await object.save();

      const schema = await config.database.loadSchema();
      // Lock the find/get, and let only owners read
      await schema.updateClass(
        'AnObject',
        { owners: { type: 'Array' } },
        { find: {}, get: {}, readUserFields: ['owners'] }
      );

      const q = new Parse.Query('AnObject');
      const objects = await q.find();
      expect(objects.length).toBe(0);

      try {
        const objects = await q.find({ useMasterKey: true });
        expect(objects.length).toBe(1);
        done();
      } catch (err) {
        done.fail('master key should find the object');
      }
    });

    it('should let master key get objects', async done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('hello', 'world');

      await object.save();
      const schema = await config.database.loadSchema();
      // Lock the find/get, and let only owners read
      await schema.updateClass(
        'AnObject',
        { owners: { type: 'Array' } },
        { find: {}, get: {}, readUserFields: ['owners'] }
      );

      const q = new Parse.Query('AnObject');
      try {
        await q.get(object.id);
        done.fail();
      } catch (err) {
        expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
      }

      try {
        const objectAgain = await q.get(object.id, { useMasterKey: true });
        expect(objectAgain).not.toBeUndefined();
        expect(objectAgain.id).toBe(object.id);
        done();
      } catch (err) {
        done.fail('master key should get the object');
      }
    });

    it('should let master key update objects', async done => {
      const config = Config.get(Parse.applicationId);
      const object = new Parse.Object('AnObject');
      object.set('hello', 'world');
      await object.save();

      const schema = await config.database.loadSchema();
      // Lock the update, and let only owners write
      await schema.updateClass(
        'AnObject',
        { owners: { type: 'Array' } },
        { update: {}, writeUserFields: ['owners'] }
      );

      try {
        await object.save({ hello: 'bar' });
        done.fail();
      } catch (err) {
        expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
      }

      try {
        const objectAgain = await object.save({ hello: 'baz' }, { useMasterKey: true });
        expect(objectAgain.get('hello')).toBe('baz');
        done();
      } catch (err) {
        done.fail('master key should save the object');
      }
    });

    it('should let master key delete objects', async done => {
      const config = Config.get(Parse.applicationId);

      const object = new Parse.Object('AnObject');
      object.set('hello', 'world');
      await object.save();

      const schema = await config.database.loadSchema();
      // Lock the delete, and let only owners write
      await schema.updateClass(
        'AnObject',
        { owners: { type: 'Array' } },
        { delete: {}, writeUserFields: ['owners'] }
      );

      try {
        await object.destroy();
        done.fail();
      } catch (err) {
        expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
      }
      try {
        await object.destroy({ useMasterKey: true });
        done();
      } catch (err) {
        done.fail('master key should destroy the object');
      }
    });

    it('should fail with invalid pointer perms (not array)', async done => {
      const config = Config.get(Parse.applicationId);
      const schema = await config.database.loadSchema();
      try {
        // Lock the delete, and let only owners write
        await schema.addClassIfNotExists(
          'AnObject',
          { owners: { type: 'Array' } },
          { delete: {}, writeUserFields: 'owners' }
        );
      } catch (err) {
        expect(err.code).toBe(Parse.Error.INVALID_JSON);
        done();
      }
    });

    it('should fail with invalid pointer perms (non-existing field)', async done => {
      const config = Config.get(Parse.applicationId);
      const schema = await config.database.loadSchema();
      try {
        // Lock the delete, and let only owners write
        await schema.addClassIfNotExists(
          'AnObject',
          { owners: { type: 'Array' } },
          { delete: {}, writeUserFields: ['owners', 'invalid'] }
        );
      } catch (err) {
        expect(err.code).toBe(Parse.Error.INVALID_JSON);
        done();
      }
    });
  });

  describe('Granular ', () => {
    const className = 'AnObject';

    const actionGet = id => new Parse.Query(className).get(id);
    const actionFind = () => new Parse.Query(className).find();
    const actionCount = () => new Parse.Query(className).count();
    const actionCreate = () => new Parse.Object(className).save();
    const actionUpdate = obj => obj.save({ revision: 2 });
    const actionDelete = obj => obj.destroy();
    const actionAddFieldOnCreate = () =>
      new Parse.Object(className, { ['extra' + Date.now()]: 'field' }).save();
    const actionAddFieldOnUpdate = obj => obj.save({ ['another' + Date.now()]: 'field' });

    const OBJECT_NOT_FOUND = new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
    const PERMISSION_DENIED = jasmine.stringMatching('Permission denied');

    async function createUser(username, password = 'password') {
      const user = new Parse.User({
        username: username + Date.now(),
        password,
      });

      await user.save();

      return user;
    }

    async function logIn(userObject) {
      return await Parse.User.logIn(userObject.getUsername(), 'password');
    }

    async function updateCLP(clp) {
      const config = Config.get(Parse.applicationId);
      const schemaController = await config.database.loadSchema();

      await schemaController.updateClass(className, {}, clp);
    }

    describe('on single-pointer fields', () => {
      /** owns: **obj1** */
      let user1;

      /** owns: **obj2** */
      let user2;

      /** owned by: **user1** */
      let obj1;

      /** owned by: **user2** */
      let obj2;

      async function initialize() {
        await Config.get(Parse.applicationId).schemaCache.clear();

        [user1, user2] = await Promise.all([createUser('user1'), createUser('user2')]);

        obj1 = new Parse.Object(className, {
          owner: user1,
          revision: 0,
        });

        obj2 = new Parse.Object(className, {
          owner: user2,
          revision: 0,
        });

        await Parse.Object.saveAll([obj1, obj2], {
          useMasterKey: true,
        });
      }

      beforeEach(async () => {
        await initialize();
      });

      describe('get action', () => {
        it('should be allowed', async done => {
          await updateCLP({
            get: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          const result = await actionGet(obj1.id);
          expect(result).toBeDefined();
          done();
        });

        it_id('9ba681d5-59f5-4996-b36d-6647d23e6a44')(it)('should fail for user not listed', async done => {
          await updateCLP({
            get: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user2);

          await expectAsync(actionGet(obj1.id)).toBeRejectedWith(OBJECT_NOT_FOUND);
          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            get: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionFind(),
              actionCount(),
              actionCreate(),
              actionUpdate(obj1),
              actionAddFieldOnCreate(),
              actionDelete(obj1),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('find action', () => {
        it('should be allowed', async done => {
          await updateCLP({
            find: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await expectAsync(actionFind()).toBeResolved();
          done();
        });

        it('should be limited to objects where user is listed in field', async done => {
          await updateCLP({
            find: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user2);

          const results = await actionFind();
          expect(results.length).toBe(1);
          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            find: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionGet(obj1.id),
              actionCount(),
              actionCreate(),
              actionUpdate(obj1),
              actionAddFieldOnCreate(),
              actionDelete(obj1),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('count action', () => {
        it('should be allowed', async done => {
          await updateCLP({
            count: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          const count = await actionCount();
          expect(count).toBe(1);
          done();
        });

        it('should be limited to objects where user is listed in field', async done => {
          await updateCLP({
            count: {
              pointerFields: ['owner'],
            },
          });

          const user3 = await createUser('user3');
          await logIn(user3);

          const p = await actionCount();
          expect(p).toBe(0);

          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            count: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionGet(obj1.id),
              actionFind(),
              actionCreate(),
              actionUpdate(obj1),
              actionAddFieldOnCreate(),
              actionDelete(obj1),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('update action', () => {
        it('should be allowed', async done => {
          await updateCLP({
            update: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);
          await expectAsync(actionUpdate(obj1)).toBeResolved();
          done();
        });

        it_id('bcdb158d-c0b6-45e3-84ab-a3636f7cb470')(it)('should fail for user not listed', async done => {
          await updateCLP({
            update: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user2);

          await expectAsync(actionUpdate(obj1)).toBeRejectedWith(OBJECT_NOT_FOUND);
          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            update: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionGet(obj1.id),
              actionFind(),
              actionCount(),
              actionCreate(),
              actionAddFieldOnCreate(),
              actionDelete(obj1),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('delete action', () => {
        it('should be allowed', async done => {
          await updateCLP({
            delete: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await expectAsync(actionDelete(obj1)).toBeResolved();
          done();
        });

        it_id('70aa3853-6e26-4c38-a927-2ddb24ced7d4')(it)('should fail for user not listed', async done => {
          await updateCLP({
            delete: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user2);

          await expectAsync(actionDelete(obj1)).toBeRejectedWith(OBJECT_NOT_FOUND);
          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            delete: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionGet(obj1.id),
              actionFind(),
              actionCount(),
              actionCreate(),
              actionUpdate(obj1),
              actionAddFieldOnCreate(),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('create action', () => {
        // For Pointer permissions create is different from other operations
        // since there's no object holding the pointer before created
        it('should be denied (writelock) when no other permissions on class', async done => {
          await updateCLP({
            create: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);
          await expectAsync(actionCreate()).toBeRejectedWith(PERMISSION_DENIED);
          done();
        });
      });

      describe('addField action', () => {
        xit('should have no effect when creating object (and allowed by explicit userid permission)', async done => {
          await updateCLP({
            create: {
              '*': true,
            },
            addField: {
              [user1.id]: true,
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await expectAsync(actionAddFieldOnCreate()).toBeResolved();
          done();
        });

        xit('should be denied when creating object (and no explicit permission)', async done => {
          await updateCLP({
            create: {
              '*': true,
            },
            addField: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          const newObject = new Parse.Object(className, {
            owner: user1,
            extra: 'field',
          });
          await expectAsync(newObject.save()).toBeRejectedWith(PERMISSION_DENIED);
          done();
        });

        it('should be allowed when updating object', async done => {
          await updateCLP({
            update: {
              '*': true,
            },
            addField: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user1);

          await expectAsync(actionAddFieldOnUpdate(obj1)).toBeResolved();

          done();
        });

        it('should be denied when updating object for user without addField permission', async done => {
          await updateCLP({
            update: {
              '*': true,
            },
            addField: {
              pointerFields: ['owner'],
            },
          });

          await logIn(user2);

          await expectAsync(actionAddFieldOnUpdate(obj1)).toBeRejectedWith(OBJECT_NOT_FOUND);

          done();
        });
      });
    });

    describe('on array of pointers', () => {
      /**
       * owns: **obj1**
       *
       * moderates: **obj1** */
      let user1;

      /**
       * owns: **obj2**
       *
       * moderates: **obj1, obj2** */
      let user2;

      /**
       * owns: **obj3**
       *
       * moderates: **obj1, obj2, obj3 ** */
      let user3;

      /**
       * owned by: **user1**
       *
       * moderated by: **user1, user2, user3** */
      let obj1;

      /**
       * owned by: **user2**
       *
       * moderated by: **user2, user3** */
      let obj2;

      /**
       * owned by: **user3**
       *
       * moderated by: **user3** */
      let obj3;

      /**
       * owned by: **noboody**
       *
       * moderated by: **nobody** */
      let objNobody;

      async function initialize() {
        await Config.get(Parse.applicationId).schemaCache.clear();

        [user1, user2, user3] = await Promise.all([
          createUser('user1'),
          createUser('user2'),
          createUser('user3'),
        ]);

        obj1 = new Parse.Object(className);
        obj2 = new Parse.Object(className);
        obj3 = new Parse.Object(className);
        objNobody = new Parse.Object(className);

        obj1.set({
          owners: [user1],
          moderators: [user3, user2, user1],
          revision: 0,
        });

        obj2.set({
          owners: [user2],
          moderators: [user3, user2],
          revision: 0,
        });

        obj3.set({
          owners: [user3],
          moderators: [user3],
          revision: 0,
        });

        objNobody.set({
          owners: [],
          moderators: [],
          revision: 0,
        });

        await Parse.Object.saveAll([obj1, obj2, obj3, objNobody], {
          useMasterKey: true,
        });
      }

      beforeEach(async () => {
        await initialize();
      });

      describe('get action', () => {
        it('should be allowed (1 user in array)', async done => {
          await updateCLP({
            get: {
              pointerFields: ['owners'],
            },
          });

          await logIn(user1);

          const result = await actionGet(obj1.id);
          expect(result).toBeDefined();
          done();
        });

        it('should be allowed (multiple users in array)', async done => {
          await updateCLP({
            get: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user2);

          const result = await actionGet(obj1.id);
          expect(result).toBeDefined();
          done();
        });

        it_id('84a42339-c7b5-4735-a431-57b46535b073')(it)('should fail for user not listed', async done => {
          await updateCLP({
            get: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);

          await expectAsync(actionGet(obj3.id)).toBeRejectedWith(OBJECT_NOT_FOUND);
          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            get: {
              pointerFields: ['owners'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionFind(),
              actionCount(),
              actionCreate(),
              actionUpdate(obj2),
              actionAddFieldOnCreate(),
              actionAddFieldOnUpdate(obj2),
              actionDelete(obj2),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('find action', () => {
        it('should be allowed (1 user in array)', async done => {
          await updateCLP({
            find: {
              pointerFields: ['owners'],
            },
          });

          await logIn(user1);

          const results = await actionFind();
          expect(results.length).toBe(1);
          done();
        });

        it('should be allowed (multiple users in array)', async done => {
          await updateCLP({
            find: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user2);

          const results = await actionFind();
          expect(results.length).toBe(2);
          done();
        });

        it('should be limited to objects where user is listed in field', async done => {
          await updateCLP({
            find: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);

          const results = await actionFind();
          expect(results.length).toBe(1);
          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            find: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionGet(obj1.id),
              actionCount(),
              actionCreate(),
              actionUpdate(obj1),
              actionAddFieldOnCreate(),
              actionAddFieldOnUpdate(obj1),
              actionDelete(obj1),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('count action', () => {
        beforeEach(async () => {
          await updateCLP({
            count: {
              pointerFields: ['moderators'],
            },
          });
        });

        it('should be allowed', async done => {
          await logIn(user1);

          const count = await actionCount();
          expect(count).toBe(1);
          done();
        });

        it('should be limited to objects where user is listed in field', async done => {
          await logIn(user2);

          const count = await actionCount();
          expect(count).toBe(2);

          done();
        });

        it('should not allow other actions', async done => {
          await logIn(user1);

          await Promise.all(
            [
              actionGet(obj1.id),
              actionFind(),
              actionCreate(),
              actionUpdate(obj1),
              actionAddFieldOnCreate(),
              actionAddFieldOnUpdate(obj1),
              actionDelete(obj1),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('update action', () => {
        it('should be allowed (1 user in array)', async done => {
          await updateCLP({
            update: {
              pointerFields: ['owners'],
            },
          });

          await logIn(user1);

          await expectAsync(actionUpdate(obj1)).toBeResolved();
          done();
        });

        it_id('2b19234a-a471-48b4-bd1a-27bd286d066f')(it)('should be allowed (multiple users in array)', async done => {
          await updateCLP({
            update: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user2);

          await expectAsync(actionUpdate(obj1)).toBeResolved();
          done();
        });

        it_id('1abb9f4a-fb24-48c7-8025-3001d6cf8737')(it)('should fail for user not listed', async done => {
          await updateCLP({
            update: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user2);

          await expectAsync(actionUpdate(obj3)).toBeRejectedWith(OBJECT_NOT_FOUND);
          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            update: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionGet(obj1.id),
              actionFind(),
              actionCount(),
              actionCreate(),
              actionAddFieldOnCreate(),
              actionAddFieldOnUpdate(obj1),
              actionDelete(obj1),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('delete action', () => {
        it('should be allowed (1 user in array)', async done => {
          await updateCLP({
            delete: {
              pointerFields: ['owners'],
            },
          });

          await logIn(user1);

          await expectAsync(actionDelete(obj1)).toBeResolved();
          done();
        });

        it('should be allowed (multiple users in array)', async done => {
          await updateCLP({
            delete: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user3);

          await expectAsync(actionDelete(obj2)).toBeResolved();
          done();
        });

        it_id('3175a0e3-e51e-4b84-a2e6-50bbcc582123')(it)('should fail for user not listed', async done => {
          await updateCLP({
            delete: {
              pointerFields: ['owners'],
            },
          });

          await logIn(user1);

          await expectAsync(actionDelete(obj3)).toBeRejectedWith(OBJECT_NOT_FOUND);
          done();
        });

        it('should not allow other actions', async done => {
          await updateCLP({
            delete: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);

          await Promise.all(
            [
              actionGet(obj1.id),
              actionFind(),
              actionCount(),
              actionCreate(),
              actionUpdate(obj1),
              actionAddFieldOnCreate(),
              actionAddFieldOnUpdate(obj1),
            ].map(async p => {
              await expectAsync(p).toBeRejectedWith(PERMISSION_DENIED);
            })
          );
          done();
        });
      });

      describe('create action', () => {
        /* For Pointer permissions 'create' is different from other operations
         since there's no object holding the pointer before created */
        it('should be denied (writelock) when no other permissions on class', async done => {
          await updateCLP({
            create: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);
          await expectAsync(actionCreate()).toBeRejectedWith(PERMISSION_DENIED);
          done();
        });
      });

      describe('addField action', () => {
        it('should have no effect on create (allowed by explicit userid)', async done => {
          await updateCLP({
            create: {
              '*': true,
            },
            addField: {
              [user1.id]: true,
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);

          await expectAsync(actionAddFieldOnCreate()).toBeResolved();
          done();
        });

        it('should be denied when creating object (and no explicit permission)', async done => {
          await updateCLP({
            create: {
              '*': true,
            },
            addField: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);

          const newObject = new Parse.Object(className, {
            moderators: user1,
            extra: 'field',
          });
          await expectAsync(newObject.save()).toBeRejectedWith(PERMISSION_DENIED);
          done();
        });

        it('should be allowed when updating object', async done => {
          await updateCLP({
            update: {
              '*': true,
            },
            addField: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user2);

          await expectAsync(actionAddFieldOnUpdate(obj1)).toBeResolved();

          done();
        });

        it_id('51e896e9-73b3-404f-b5ff-bdb99005a9f7')(it)('should be restricted when updating object without addField permission', async done => {
          await updateCLP({
            update: {
              '*': true,
            },
            addField: {
              pointerFields: ['moderators'],
            },
          });

          await logIn(user1);

          await expectAsync(actionAddFieldOnUpdate(obj2)).toBeRejectedWith(OBJECT_NOT_FOUND);

          done();
        });
      });
    });

    describe('combined with grouped', () => {
      /**
       * owns: **obj1**
       *
       * moderates: **obj2** */
      let user1;

      /**
       * owns: **obj2**
       *
       * moderates: **obj1, obj2** */
      let user2;

      /**
       * owned by: **user1**
       *
       * moderated by: **user2** */
      let obj1;

      /**
       * owned by: **user2**
       *
       * moderated by: **user1, user2** */
      let obj2;

      async function initialize() {
        await Config.get(Parse.applicationId).schemaCache.clear();

        [user1, user2] = await Promise.all([createUser('user1'), createUser('user2')]);

        // User1 owns object1
        // User2 owns object2
        obj1 = new Parse.Object(className, {
          owner: user1,
          moderators: [user2],
          revision: 0,
        });

        obj2 = new Parse.Object(className, {
          owner: user2,
          moderators: [user1, user2],
          revision: 0,
        });

        await Parse.Object.saveAll([obj1, obj2], {
          useMasterKey: true,
        });
      }

      beforeEach(async () => {
        await initialize();
      });

      it_id('b43db366-8cce-4a11-9cf2-eeee9603d40b')(it)('should not limit the scope of grouped read permissions', async done => {
        await updateCLP({
          get: {
            pointerFields: ['owner'],
          },
          readUserFields: ['moderators'],
        });

        await logIn(user2);

        await expectAsync(actionGet(obj1.id)).toBeResolved();

        const found = await actionFind();
        expect(found.length).toBe(2);

        const counted = await actionCount();
        expect(counted).toBe(2);

        done();
      });

      it_id('bbb1686d-0e2a-4365-8b64-b5faa3e7b9cf')(it)('should not limit the scope of grouped write permissions', async done => {
        await updateCLP({
          update: {
            pointerFields: ['owner'],
          },
          writeUserFields: ['moderators'],
        });

        await logIn(user2);

        await expectAsync(actionUpdate(obj1)).toBeResolved();
        await expectAsync(actionAddFieldOnUpdate(obj1)).toBeResolved();
        await expectAsync(actionDelete(obj1)).toBeResolved();
        // [create] and [addField on create] can't be enabled with pointer by design

        done();
      });

      it('should not inherit scope of grouped read permissions from another field', async done => {
        await updateCLP({
          get: {
            pointerFields: ['owner'],
          },
          readUserFields: ['moderators'],
        });

        await logIn(user1);

        const found = await actionFind();
        expect(found.length).toBe(1);

        const counted = await actionCount();
        expect(counted).toBe(1);

        done();
      });

      it('should not inherit scope of grouped write permissions from another field', async done => {
        await updateCLP({
          update: {
            pointerFields: ['moderators'],
          },
          writeUserFields: ['owner'],
        });

        await logIn(user1);

        await expectAsync(actionDelete(obj2)).toBeRejectedWith(OBJECT_NOT_FOUND);

        done();
      });
    });

    describe('using pointer-fields and queries with keys projection', () => {
      let user1;
      /**
       * owner: user1
       *
       * testers: [user1]
       */
      let obj;

      /**
       * Clear cache, create user and object, login user
       */
      async function initialize() {
        await Config.get(Parse.applicationId).schemaCache.clear();

        user1 = await createUser('user1');
        user1 = await logIn(user1);

        obj = new Parse.Object(className);

        obj.set('owner', user1);
        obj.set('field', 'field');
        obj.set('test', 'test');

        await Parse.Object.saveAll([obj], { useMasterKey: true });

        await obj.fetch();
      }

      beforeEach(async () => {
        await initialize();
      });

      it('should be enforced regardless of pointer-field being included in keys (select)', async done => {
        await updateCLP({
          get: { '*': true },
          find: { pointerFields: ['owner'] },
          update: { pointerFields: ['owner'] },
        });

        const query = new Parse.Query('AnObject');
        query.select('field', 'test');

        const [object] = await query.find({ objectId: obj.id });
        expect(object.get('field')).toBe('field');
        expect(object.get('test')).toBe('test');
        done();
      });
    });
  });
});
