const Config = require('../lib/Config');
const Parse = require('parse/node');
const request = require('../lib/request');
const { className, createRole, createUser, logIn, updateCLP } = require('./support/dev');

describe('ProtectedFields', function () {
  it('should handle and empty protectedFields', async function () {
    const protectedFields = {};
    await reconfigureServer({ protectedFields });

    const user = new Parse.User();
    user.setUsername('Alice');
    user.setPassword('sekrit');
    user.set('email', 'alice@aol.com');
    user.set('favoriteColor', 'yellow');
    const acl = new Parse.ACL();
    acl.setPublicReadAccess(true);
    user.setACL(acl);
    await user.save();

    const fetched = await new Parse.Query(Parse.User).get(user.id);
    expect(fetched.has('email')).toBeFalsy();
    expect(fetched.has('favoriteColor')).toBeTruthy();
  });

  describe('interaction with legacy userSensitiveFields', function () {
    it('should fall back on sensitive fields if protected fields are not configured', async function () {
      const userSensitiveFields = ['phoneNumber', 'timeZone'];

      const protectedFields = { _User: { '*': ['email'] } };

      await reconfigureServer({ userSensitiveFields, protectedFields });
      const user = new Parse.User();
      user.setUsername('Alice');
      user.setPassword('sekrit');
      user.set('email', 'alice@aol.com');
      user.set('phoneNumber', 8675309);
      user.set('timeZone', 'America/Los_Angeles');
      user.set('favoriteColor', 'yellow');
      user.set('favoriteFood', 'pizza');
      const acl = new Parse.ACL();
      acl.setPublicReadAccess(true);
      user.setACL(acl);
      await user.save();

      const fetched = await new Parse.Query(Parse.User).get(user.id);
      expect(fetched.has('email')).toBeFalsy();
      expect(fetched.has('phoneNumber')).toBeFalsy();
      expect(fetched.has('favoriteColor')).toBeTruthy();
    });

    it('should merge protected and sensitive for extra safety', async function () {
      const userSensitiveFields = ['phoneNumber', 'timeZone'];

      const protectedFields = { _User: { '*': ['email', 'favoriteFood'] } };

      await reconfigureServer({ userSensitiveFields, protectedFields });
      const user = new Parse.User();
      user.setUsername('Alice');
      user.setPassword('sekrit');
      user.set('email', 'alice@aol.com');
      user.set('phoneNumber', 8675309);
      user.set('timeZone', 'America/Los_Angeles');
      user.set('favoriteColor', 'yellow');
      user.set('favoriteFood', 'pizza');
      const acl = new Parse.ACL();
      acl.setPublicReadAccess(true);
      user.setACL(acl);
      await user.save();

      const fetched = await new Parse.Query(Parse.User).get(user.id);
      expect(fetched.has('email')).toBeFalsy();
      expect(fetched.has('phoneNumber')).toBeFalsy();
      expect(fetched.has('favoriteFood')).toBeFalsy();
      expect(fetched.has('favoriteColor')).toBeTruthy();
    });
  });

  describe('non user class', function () {
    it('should hide fields in a non user class', async function () {
      const protectedFields = {
        ClassA: { '*': ['foo'] },
        ClassB: { '*': ['bar'] },
      };
      await reconfigureServer({ protectedFields });

      const objA = await new Parse.Object('ClassA').set('foo', 'zzz').set('bar', 'yyy').save();

      const objB = await new Parse.Object('ClassB').set('foo', 'zzz').set('bar', 'yyy').save();

      const [fetchedA, fetchedB] = await Promise.all([
        new Parse.Query('ClassA').get(objA.id),
        new Parse.Query('ClassB').get(objB.id),
      ]);

      expect(fetchedA.has('foo')).toBeFalsy();
      expect(fetchedA.has('bar')).toBeTruthy();

      expect(fetchedB.has('foo')).toBeTruthy();
      expect(fetchedB.has('bar')).toBeFalsy();
    });

    it('should hide fields in non user class and non standard user field at same time', async function () {
      const protectedFields = {
        _User: { '*': ['phoneNumber'] },
        ClassA: { '*': ['foo'] },
        ClassB: { '*': ['bar'] },
      };

      await reconfigureServer({ protectedFields });

      const user = new Parse.User();
      user.setUsername('Alice');
      user.setPassword('sekrit');
      user.set('email', 'alice@aol.com');
      user.set('phoneNumber', 8675309);
      user.set('timeZone', 'America/Los_Angeles');
      user.set('favoriteColor', 'yellow');
      user.set('favoriteFood', 'pizza');
      const acl = new Parse.ACL();
      acl.setPublicReadAccess(true);
      user.setACL(acl);
      await user.save();

      const objA = await new Parse.Object('ClassA').set('foo', 'zzz').set('bar', 'yyy').save();

      const objB = await new Parse.Object('ClassB').set('foo', 'zzz').set('bar', 'yyy').save();

      const [fetchedUser, fetchedA, fetchedB] = await Promise.all([
        new Parse.Query(Parse.User).get(user.id),
        new Parse.Query('ClassA').get(objA.id),
        new Parse.Query('ClassB').get(objB.id),
      ]);

      expect(fetchedA.has('foo')).toBeFalsy();
      expect(fetchedA.has('bar')).toBeTruthy();

      expect(fetchedB.has('foo')).toBeTruthy();
      expect(fetchedB.has('bar')).toBeFalsy();

      expect(fetchedUser.has('email')).toBeFalsy();
      expect(fetchedUser.has('phoneNumber')).toBeFalsy();
      expect(fetchedUser.has('favoriteColor')).toBeTruthy();
    });
  });

  describe('using the pointer-permission variant', () => {
    let user1, user2;
    beforeEach(async () => {
      Config.get(Parse.applicationId).schemaCache.clear();
      user1 = await Parse.User.signUp('user1', 'password');
      user2 = await Parse.User.signUp('user2', 'password');
      await Parse.User.logOut();
    });

    describe('and get/fetch', () => {
      it('should allow access using single user pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');

        obj.set('owner', user1);
        obj.set('test', 'test');
        await obj.save();

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: { '*': ['owner'], 'userField:owner': [] },
          }
        );

        await Parse.User.logIn('user1', 'password');
        const objectAgain = await obj.fetch();
        expect(objectAgain.get('owner').id).toBe(user1.id);
        expect(objectAgain.get('test')).toBe('test');
        done();
      });

      it('should deny access to other users using single user pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');

        obj.set('owner', user1);
        obj.set('test', 'test');
        await obj.save();

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: { '*': ['owner'], 'userField:owner': [] },
          }
        );

        await Parse.User.logIn('user2', 'password');
        const objectAgain = await obj.fetch();
        expect(objectAgain.get('owner')).toBe(undefined);
        expect(objectAgain.get('test')).toBe('test');
        done();
      });

      it('should deny access to public using single user pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');

        obj.set('owner', user1);
        obj.set('test', 'test');
        await obj.save();

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: { '*': ['owner'], 'userField:owner': [] },
          }
        );

        const objectAgain = await obj.fetch();
        expect(objectAgain.get('owner')).toBe(undefined);
        expect(objectAgain.get('test')).toBe('test');
        done();
      });

      it('should allow access using user array pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');

        obj.set('owners', [user1, user2]);
        obj.set('test', 'test');
        await obj.save();

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

        await Parse.User.logIn('user1', 'password');
        let objectAgain = await obj.fetch();
        expect(objectAgain.get('owners')[0].id).toBe(user1.id);
        expect(objectAgain.get('test')).toBe('test');
        await Parse.User.logIn('user2', 'password');
        objectAgain = await obj.fetch();
        expect(objectAgain.get('owners')[1].id).toBe(user2.id);
        expect(objectAgain.get('test')).toBe('test');
        done();
      });

      it('should deny access to other users using user array pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');

        obj.set('owners', [user1]);
        obj.set('test', 'test');
        await obj.save();

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

        await Parse.User.logIn('user2', 'password');
        const objectAgain = await obj.fetch();
        expect(objectAgain.get('owners')).toBe(undefined);
        expect(objectAgain.get('test')).toBe('test');
        done();
      });

      it('should deny access to public using user array pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');

        obj.set('owners', [user1, user2]);
        obj.set('test', 'test');
        await obj.save();

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

        const objectAgain = await obj.fetch();
        expect(objectAgain.get('owners')).toBe(undefined);
        expect(objectAgain.get('test')).toBe('test');
        done();
      });

      it('should intersect protected fields when using multiple pointer-permission fields', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');

        obj.set('owners', [user1]);
        obj.set('owner', user1);
        obj.set('test', 'test');
        await obj.save();

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: {
              '*': ['owners', 'owner', 'test'],
              'userField:owners': ['owners', 'owner'],
              'userField:owner': ['owner'],
            },
          }
        );

        // Check if protectFields from pointer-permissions got combined
        await Parse.User.logIn('user1', 'password');
        const objectAgain = await obj.fetch();
        expect(objectAgain.get('owners').length).toBe(1);
        expect(objectAgain.get('owner')).toBe(undefined);
        expect(objectAgain.get('test')).toBe('test');
        done();
      });

      it('should ignore pointer-permission fields not present in object', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');

        obj.set('owners', [user1]);
        obj.set('owner', user1);
        obj.set('test', 'test');
        await obj.save();

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: {
              '*': [],
              'userField:idontexist': ['owner'],
              'userField:idontexist2': ['owners'],
            },
          }
        );

        await Parse.User.logIn('user1', 'password');
        const objectAgain = await obj.fetch();
        expect(objectAgain.get('owners')).not.toBe(undefined);
        expect(objectAgain.get('owner')).not.toBe(undefined);
        expect(objectAgain.get('test')).toBe('test');
        done();
      });
    });

    describe('and find', () => {
      it('should allow access using single user pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');

        obj.set('owner', user1);
        obj.set('test', 'test');
        obj2.set('owner', user1);
        obj2.set('test', 'test2');
        await Parse.Object.saveAll([obj, obj2]);

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: { '*': ['owner'], 'userField:owner': [] },
          }
        );

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

        const q = new Parse.Query('AnObject');
        const results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owner').id).toBe(user1.id);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owner').id).toBe(user1.id);
        expect(results[1].get('test')).toBe('test2');
        done();
      });

      it('should deny access to other users using single user pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');

        obj.set('owner', user1);
        obj.set('test', 'test');
        obj2.set('owner', user1);
        obj2.set('test', 'test2');
        await Parse.Object.saveAll([obj, obj2]);

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: { '*': ['owner'], 'userField:owner': [] },
          }
        );

        await Parse.User.logIn('user2', 'password');
        const q = new Parse.Query('AnObject');
        const results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owner')).toBe(undefined);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owner')).toBe(undefined);
        expect(results[1].get('test')).toBe('test2');
        done();
      });

      it('should deny access to public using single user pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');

        obj.set('owner', user1);
        obj.set('test', 'test');
        obj2.set('owner', user1);
        obj2.set('test', 'test2');
        await Parse.Object.saveAll([obj, obj2]);

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: { '*': ['owner'], 'userField:owner': [] },
          }
        );

        const q = new Parse.Query('AnObject');
        const results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owner')).toBe(undefined);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owner')).toBe(undefined);
        expect(results[1].get('test')).toBe('test2');
        done();
      });

      it('should allow access using user array pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');

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

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

        const q = new Parse.Query('AnObject');
        let results;

        await Parse.User.logIn('user1', 'password');
        results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owners')[0].id).toBe(user1.id);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owners')[0].id).toBe(user1.id);
        expect(results[1].get('test')).toBe('test2');

        await Parse.User.logIn('user2', 'password');
        results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owners')[1].id).toBe(user2.id);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owners')[1].id).toBe(user2.id);
        expect(results[1].get('test')).toBe('test2');
        done();
      });

      it('should deny access to other users using user array pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');

        obj.set('owners', [user1]);
        obj.set('test', 'test');
        obj2.set('owners', [user1]);
        obj2.set('test', 'test2');
        await Parse.Object.saveAll([obj, obj2]);

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

        await Parse.User.logIn('user2', 'password');
        const q = new Parse.Query('AnObject');
        const results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owners')).toBe(undefined);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owners')).toBe(undefined);
        expect(results[1].get('test')).toBe('test2');
        done();
      });

      it('should deny access to public using user array pointer-permissions', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');

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

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

        const q = new Parse.Query('AnObject');
        const results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owners')).toBe(undefined);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owners')).toBe(undefined);
        expect(results[1].get('test')).toBe('test2');
        done();
      });

      it('should intersect protected fields when using multiple pointer-permission fields', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');

        obj.set('owners', [user1]);
        obj.set('owner', user1);
        obj.set('test', 'test');
        obj2.set('owners', [user1]);
        obj2.set('test', 'test2');
        await Parse.Object.saveAll([obj, obj2]);

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: {
              '*': ['owners', 'owner', 'test'],
              'userField:owners': ['owners', 'owner'],
              'userField:owner': ['owner'],
            },
          }
        );

        // Check if protectFields from pointer-permissions got combined
        await Parse.User.logIn('user1', 'password');

        const q = new Parse.Query('AnObject');
        const results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owners').length).toBe(1);
        expect(results[0].get('owner')).toBe(undefined);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owners')).toBe(undefined);
        expect(results[1].get('owner')).toBe(undefined);
        expect(results[1].get('test')).toBe('test2');
        done();
      });

      it('should ignore pointer-permission fields not present in object', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');

        obj.set('owners', [user1]);
        obj.set('owner', user1);
        obj.set('test', 'test');
        obj2.set('owners', [user1]);
        obj2.set('owner', user1);
        obj2.set('test', 'test2');
        await Parse.Object.saveAll([obj, obj2]);

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: {
              '*': [],
              'userField:idontexist': ['owner'],
              'userField:idontexist2': ['owners'],
            },
          }
        );

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

        const q = new Parse.Query('AnObject');
        const results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(2);

        expect(results[0].get('owners')).not.toBe(undefined);
        expect(results[0].get('owner')).not.toBe(undefined);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owners')).not.toBe(undefined);
        expect(results[1].get('owner')).not.toBe(undefined);
        expect(results[1].get('test')).toBe('test2');
        done();
      });

      it('should filter only fields from objects not owned by the user', async done => {
        const config = Config.get(Parse.applicationId);
        const obj = new Parse.Object('AnObject');
        const obj2 = new Parse.Object('AnObject');
        const obj3 = new Parse.Object('AnObject');

        obj.set('owner', user1);
        obj.set('test', 'test');
        obj2.set('owner', user2);
        obj2.set('test', 'test2');
        obj3.set('owner', user2);
        obj3.set('test', 'test3');
        await Parse.Object.saveAll([obj, obj2, obj3]);

        const schema = await config.database.loadSchema();
        await schema.updateClass(
          'AnObject',
          {},
          {
            get: { '*': true },
            find: { '*': true },
            protectedFields: {
              '*': ['owner'],
              'userField:owner': [],
            },
          }
        );

        const q = new Parse.Query('AnObject');
        let results;

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

        results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(3);

        expect(results[0].get('owner')).not.toBe(undefined);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owner')).toBe(undefined);
        expect(results[1].get('test')).toBe('test2');
        expect(results[2].get('owner')).toBe(undefined);
        expect(results[2].get('test')).toBe('test3');

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

        results = await q.find();
        // sort for checking in correct order
        results.sort((a, b) => a.get('test').localeCompare(b.get('test')));
        expect(results.length).toBe(3);

        expect(results[0].get('owner')).toBe(undefined);
        expect(results[0].get('test')).toBe('test');
        expect(results[1].get('owner')).not.toBe(undefined);
        expect(results[1].get('test')).toBe('test2');
        expect(results[2].get('owner')).not.toBe(undefined);
        expect(results[2].get('test')).toBe('test3');
        done();
      });
    });
  });

  describe('schema setup', () => {
    let object;

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

      object = new Parse.Object(className);

      object.set('revision', 0);
      object.set('test', 'test');

      await object.save(null, { useMasterKey: true });
    }

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

    it('should fail setting non-existing protected field', async done => {
      const field = 'non-existing';
      const entity = '*';

      await expectAsync(
        updateCLP({
          protectedFields: {
            [entity]: [field],
          },
        })
      ).toBeRejectedWith(
        new Parse.Error(
          Parse.Error.INVALID_JSON,
          `Field '${field}' in protectedFields:${entity} does not exist`
        )
      );
      done();
    });

    it('should allow setting authenticated', async () => {
      await expectAsync(
        updateCLP({
          protectedFields: {
            authenticated: ['test'],
          },
        })
      ).toBeResolved();
    });

    it('should not allow protecting default fields', async () => {
      const defaultFields = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
      for (const field of defaultFields) {
        await expectAsync(
          updateCLP({
            protectedFields: {
              '*': [field],
            },
          })
        ).toBeRejectedWith(
          new Parse.Error(Parse.Error.INVALID_JSON, `Default field '${field}' can not be protected`)
        );
      }
    });
  });

  describe('targeting public access', () => {
    let obj1;

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

      obj1 = new Parse.Object(className);

      obj1.set('foo', 'foo');
      obj1.set('bar', 'bar');
      obj1.set('qux', 'qux');

      await obj1.save(null, {
        useMasterKey: true,
      });
    }

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

    it('should hide field', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['foo'],
        },
      });

      // unauthenticated
      const object = await obj1.fetch();

      expect(object.get('foo')).toBe(undefined);
      expect(object.get('bar')).toBeDefined();
      expect(object.get('qux')).toBeDefined();

      done();
    });

    it('should hide mutiple fields', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['foo', 'bar'],
        },
      });

      // unauthenticated
      const object = await obj1.fetch();

      expect(object.get('foo')).toBe(undefined);
      expect(object.get('bar')).toBe(undefined);
      expect(object.get('qux')).toBeDefined();

      done();
    });

    it('should not hide any fields when set as empty array', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': [],
        },
      });

      // unauthenticated
      const object = await obj1.fetch();

      expect(object.get('foo')).toBeDefined();
      expect(object.get('bar')).toBeDefined();
      expect(object.get('qux')).toBeDefined();
      expect(object.id).toBeDefined();
      expect(object.createdAt).toBeDefined();
      expect(object.updatedAt).toBeDefined();
      expect(object.getACL()).toBeDefined();

      done();
    });
  });

  describe('targeting authenticated', () => {
    /**
     * is **owner** of: _obj1_
     *
     * is **tester** of: [ _obj1, obj2_ ]
     */
    let user1;

    /**
     * is **owner** of:  _obj2_
     *
     * is **tester** of: [ _obj1_ ]
     */
    let user2;

    /**
     * **owner**: _user1_
     *
     * **testers**: [ _user1,user2_ ]
     */
    let obj1;

    /**
     * **owner**: _user2_
     *
     * **testers**: [ _user1_ ]
     */
    let obj2;

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

      await Parse.User.logOut();

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

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

      obj1.set('owner', user1);
      obj1.set('testers', [user1, user2]);
      obj1.set('test', 'test');

      obj2.set('owner', user2);
      obj2.set('testers', [user1]);
      obj2.set('test', 'test');

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

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

    it('should not hide any fields when set as empty array', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          authenticated: [],
        },
      });

      // authenticated
      await logIn(user1);

      const object = await obj1.fetch();

      expect(object.get('owner')).toBeDefined();
      expect(object.get('testers')).toBeDefined();
      expect(object.get('test')).toBeDefined();
      expect(object.id).toBeDefined();
      expect(object.createdAt).toBeDefined();
      expect(object.updatedAt).toBeDefined();
      expect(object.getACL()).toBeDefined();

      done();
    });

    it('should hide fields for authenticated users only (* not set)', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          authenticated: ['test'],
        },
      });

      // not authenticated
      const objectNonAuth = await obj1.fetch();

      expect(objectNonAuth.get('test')).toBeDefined();

      // authenticated
      await logIn(user1);
      const object = await obj1.fetch();

      expect(object.get('test')).toBe(undefined);

      done();
    });

    it('should intersect public and auth for authenticated user', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['owner', 'testers'],
          authenticated: ['testers'],
        },
      });

      // authenticated
      await logIn(user1);
      const objectAuth = await obj1.fetch();

      // ( {A,B} intersect {B} ) == {B}

      expect(objectAuth.get('testers')).not.toBeDefined(
        'Should not be visible - protected for * and authenticated'
      );
      expect(objectAuth.get('test')).toBeDefined(
        'Should be visible - not protected for everyone (* and authenticated)'
      );
      expect(objectAuth.get('owner')).toBeDefined(
        'Should be visible - not protected for authenticated'
      );

      done();
    });

    it('should have higher prio than public for logged in users (intersect)', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['test'],
          authenticated: [],
        },
      });
      // authenticated, permitted
      await logIn(user1);

      const object = await obj1.fetch();
      expect(object.get('test')).toBe('test');

      done();
    });

    it('should have no effect on unauthenticated users (public not set)', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          authenticated: ['test'],
        },
      });

      // unauthenticated, protected
      const objectNonAuth = await obj1.fetch();
      expect(objectNonAuth.get('test')).toBe('test');

      done();
    });

    it('should protect multiple fields for authenticated users', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          authenticated: ['test', 'owner'],
        },
      });

      // authenticated
      await logIn(user1);
      const object = await obj1.fetch();

      expect(object.get('test')).toBe(undefined);
      expect(object.get('owner')).toBe(undefined);

      done();
    });

    it('should not be affected by rules not applicable to user (smoke)', async done => {
      const role = await createRole({ users: user1 });
      const roleName = role.get('name');

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          authenticated: ['owner', 'testers'],
          [`role:${roleName}`]: ['test'],
          'userField:owner': [],
          [user1.id]: [],
        },
      });

      // authenticated, non-owner, no role
      await logIn(user2);
      const objectNotOwned = await obj1.fetch();

      expect(objectNotOwned.get('owner')).toBe(undefined);
      expect(objectNotOwned.get('testers')).toBe(undefined);
      expect(objectNotOwned.get('test')).toBeDefined();

      done();
    });
  });

  describe('targeting roles', () => {
    let user1, user2;

    /**
     * owner: user1
     *
     * testers: [user1,user2]
     */
    let obj1;

    /**
     * owner: user2
     *
     * testers: [user1]
     */
    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);
      obj2 = new Parse.Object(className);

      obj1.set('owner', user1);
      obj1.set('testers', [user1, user2]);
      obj1.set('test', 'test');

      obj2.set('owner', user2);
      obj2.set('testers', [user1]);
      obj2.set('test', 'test');

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

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

    it('should hide field when user belongs to a role', async done => {
      const role = await createRole({ users: user1 });
      const roleName = role.get('name');

      await updateCLP({
        protectedFields: {
          [`role:${roleName}`]: ['test'],
        },
        get: { '*': true },
        find: { '*': true },
      });

      // user has role
      await logIn(user1);

      const object = await obj1.fetch();
      expect(object.get('test')).toBe(undefined); //  field protected
      expect(object.get('owner')).toBeDefined();
      expect(object.get('testers')).toBeDefined();

      done();
    });

    it('should not hide any fields when set as empty array', async done => {
      const role = await createRole({ users: user1 });
      const roleName = role.get('name');

      await updateCLP({
        protectedFields: {
          [`role:${roleName}`]: [],
        },
        get: { '*': true },
        find: { '*': true },
      });

      // user has role
      await logIn(user1);

      const object = await obj1.fetch();

      expect(object.get('owner')).toBeDefined();
      expect(object.get('testers')).toBeDefined();
      expect(object.get('test')).toBeDefined();
      expect(object.id).toBeDefined();
      expect(object.createdAt).toBeDefined();
      expect(object.updatedAt).toBeDefined();
      expect(object.getACL()).toBeDefined();

      done();
    });

    it('should hide multiple fields when user belongs to a role', async done => {
      const role = await createRole({ users: user1 });
      const roleName = role.get('name');

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          [`role:${roleName}`]: ['test', 'owner'],
        },
      });

      // user has role
      await logIn(user1);

      const object = await obj1.fetch();

      expect(object.get('test')).toBe(undefined, 'Field should not be visible - protected by role');
      expect(object.get('owner')).toBe(
        undefined,
        'Field should not be visible - protected by role'
      );
      expect(object.get('testers')).toBeDefined();

      done();
    });

    it('should not protect when user does not belong to a role', async done => {
      const role = await createRole({ users: user1 });
      const roleName = role.get('name');

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          [`role:${roleName}`]: ['test', 'owner'],
        },
      });

      // user doesn't have role
      await logIn(user2);
      const object = await obj1.fetch();

      expect(object.get('test')).toBeDefined();
      expect(object.get('owner')).toBeDefined();
      expect(object.get('testers')).toBeDefined();

      done();
    });

    it('should intersect protected fields when user belongs to multiple roles', async done => {
      const role1 = await createRole({ users: user1 });
      const role2 = await createRole({ users: user1 });

      const role1name = role1.get('name');
      const role2name = role2.get('name');

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          [`role:${role1name}`]: ['owner'],
          [`role:${role2name}`]: ['test', 'owner'],
        },
      });

      // user has both roles
      await logIn(user1);
      const object = await obj1.fetch();

      // "owner" is a result of intersection
      expect(object.get('owner')).toBe(
        undefined,
        'Must not be visible - protected for all roles the user belongs to'
      );
      expect(object.get('test')).toBeDefined(
        'Has to be visible - is not protected for users with role1'
      );
      done();
    });

    it('should intersect protected fields when user belongs to multiple roles hierarchy', async done => {
      const admin = await createRole({
        users: user1,
        roleName: 'admin',
      });

      const moder = await createRole({
        users: [user1, user2],
        roleName: 'moder',
      });

      const tester = await createRole({
        roleName: 'tester',
      });

      // admin supersets moder role
      moder.relation('roles').add(admin);
      await moder.save(null, { useMasterKey: true });

      tester.relation('roles').add(moder);
      await tester.save(null, { useMasterKey: true });

      const roleAdmin = `role:${admin.get('name')}`;
      const roleModer = `role:${moder.get('name')}`;
      const roleTester = `role:${tester.get('name')}`;

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          [roleAdmin]: [],
          [roleModer]: ['owner'],
          [roleTester]: ['test', 'owner'],
        },
      });

      // user1 has admin & moder & tester roles, (moder includes tester).
      await logIn(user1);
      const object = await obj1.fetch();

      // being admin makes all fields visible
      expect(object.get('test')).toBeDefined(
        'Should be visible - admin role explicitly removes protection for all fields ( [] )'
      );
      expect(object.get('owner')).toBeDefined(
        'Should be visible - admin role explicitly removes protection for all fields ( [] )'
      );

      // user2 has moder & tester role, moder includes tester.
      await logIn(user2);
      const objectAgain = await obj1.fetch();

      // being moder allows "test" field
      expect(objectAgain.get('owner')).toBe(
        undefined,
        '"owner" should not be visible - protected for each role user belongs to'
      );
      expect(objectAgain.get('test')).toBeDefined(
        'Should be visible - moder role does not protect "test" field'
      );

      done();
    });

    it('should be able to clear protected fields for role (protected for authenticated)', async done => {
      const role = await createRole({ users: user1 });
      const roleName = role.get('name');

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          authenticated: ['test'],
          [`role:${roleName}`]: [],
        },
      });

      // user has role, test field visible
      await logIn(user1);
      const object = await obj1.fetch();
      expect(object.get('test')).toBe('test');

      done();
    });

    it('should determine protectedFields as intersection of field sets for public and role', async done => {
      const role = await createRole({ users: user1 });
      const roleName = role.get('name');

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['test', 'owner'],
          [`role:${roleName}`]: ['owner', 'testers'],
        },
      });

      // user has role
      await logIn(user1);

      const object = await obj1.fetch();
      expect(object.get('test')).toBeDefined(
        'Should be visible - "test" is not protected for role user belongs to'
      );
      expect(object.get('testers')).toBeDefined(
        'Should be visible - "testers" is allowed for everyone (*)'
      );
      expect(object.get('owner')).toBe(
        undefined,
        'Should not be visible - "test" is not allowed for both public(*) and role'
      );
      done();
    });

    it('should be determined as an intersection of protecedFields for authenticated and role', async done => {
      const role = await createRole({ users: user1 });
      const roleName = role.get('name');

      // this is an example of misunderstood configuration.
      // If you allow (== do not restrict) some field for broader audience
      // (having a role implies user inheres to 'authenticated' group)
      // it's not possible to narrow by protecting field for a role.
      // You'd have to protect it for 'authenticated' as well.
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          authenticated: ['test'],
          [`role:${roleName}`]: ['owner'],
        },
      });

      // user has role
      await logIn(user1);
      const object = await obj1.fetch();

      //
      expect(object.get('test')).toBeDefined(
        "Being both auhenticated and having a role leads to clearing protection on 'test' (by role rules)"
      );
      expect(object.get('owner')).toBeDefined('All authenticated users allowed to see "owner"');
      expect(object.get('testers')).toBeDefined();

      done();
    });

    it('should not hide fields when user does not belong to a role protectedFields set for', async done => {
      const role = await createRole({ users: user2 });
      const roleName = role.get('name');

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          [`role:${roleName}`]: ['test'],
        },
      });

      // relate user1 to some role, no protectedFields for it
      await createRole({ users: user1 });

      await logIn(user1);

      const object = await obj1.fetch();
      expect(object.get('test')).toBeDefined(
        'Field should be visible - user belongs to a role that has no protectedFields set'
      );

      done();
    });
  });

  describe('using pointer-fields and queries with keys projection', () => {
    /*
     * Pointer variant ("userField:column") relies on User ids
     * returned after query executed (hides fields before sending it to client)
     * If such column is excluded/not included (not returned from db because of 'project')
     * there will be no user ids to check against
     * and protectedFields won't be applied correctly.
     */

    let user1;
    /**
     * owner: user1
     *
     * testers: [user1]
     */
    let obj;

    let headers;

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

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

      // await user1.fetch();
      obj = new Parse.Object(className);

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

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

      headers = {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-Rest-API-Key': 'rest',
        'Content-Type': 'application/json',
        'X-Parse-Session-Token': user1.getSessionToken(),
      };
    }

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

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

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

      const object = await query.get(obj.id);
      expect(object.get('field')).toBe('field');
      expect(object.get('test')).toBe('test');
      done();
    });

    it('should protect fields for query where pointer field is not included via keys (REST GET)', async done => {
      const 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 updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['field', 'test'],
          'userField:owner': ['test'],
        },
      });

      const { data: object } = await request({
        url: `${Parse.serverURL}/classes/${className}/${obj.id}`,
        qs: {
          keys: 'field,test',
        },
        headers: headers,
      });

      expect(object.field).toBe(
        'field',
        'Should BE in response - not protected by "userField:owner"'
      );
      expect(object.test).toBe(
        undefined,
        'Should NOT be in response - protected by "userField:owner"'
      );
      expect(object.owner).toBe(undefined, 'Should not be in response - not included in "keys"');
      done();
    });

    it('should protect fields for query where pointer field is not included via keys (REST FIND)', async done => {
      const 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();

      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['field', 'test'],
          'userField:owner': ['test'],
        },
      });

      const { data } = await request({
        url: `${Parse.serverURL}/classes/${className}`,
        qs: {
          keys: 'field,test',
          where: JSON.stringify({ objectId: obj.id }),
        },
        headers,
      });

      const object = data.results[0];

      expect(object.field).toBe(
        'field',
        'Should be in response - not protected by "userField:owner"'
      );
      expect(object.test).toBe(
        undefined,
        'Should not be in response - protected by "userField:owner"'
      );
      expect(object.owner).toBe(undefined, 'Should not be in response - not included in "keys"');
      done();
    });

    it('should protect fields for query where pointer field is in excludeKeys (REST GET)', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['field', 'test'],
          'userField:owner': ['test'],
        },
      });

      const { data: object } = await request({
        qs: {
          excludeKeys: 'owner',
        },
        headers,
        url: `${Parse.serverURL}/classes/${className}/${obj.id}`,
      });

      expect(object.field).toBe(
        'field',
        'Should be in response - not protected by "userField:owner"'
      );
      expect(object['test']).toBe(
        undefined,
        'Should not be in response - protected by "userField:owner"'
      );
      expect(object['owner']).toBe(undefined, 'Should not be in response - not included in "keys"');
      done();
    });

    it('should protect fields for query where pointer field is in excludedKeys (REST FIND)', async done => {
      await updateCLP({
        protectedFields: {
          '*': ['field', 'test'],
          'userField:owner': ['test'],
        },
        get: { '*': true },
        find: { '*': true },
      });

      const { data } = await request({
        qs: {
          excludeKeys: 'owner',
          where: JSON.stringify({ objectId: obj.id }),
        },
        headers,
        url: `${Parse.serverURL}/classes/${className}`,
      });

      const object = data.results[0];

      expect(object.field).toBe(
        'field',
        'Should be in response - not protected by "userField:owner"'
      );
      expect(object.test).toBe(
        undefined,
        'Should not be in response - protected by "userField:owner"'
      );
      expect(object.owner).toBe(undefined, 'Should not be in response - not included in "keys"');
      done();
    });

    xit('todo: should be enforced regardless of pointer-field being excluded', async done => {
      await updateCLP({
        get: { '*': true },
        find: { '*': true },
        protectedFields: {
          '*': ['field', 'test'],
          'userField:owner': [],
        },
      });

      const query = new Parse.Query('AnObject');

      /* TODO: this has some caching problems on JS-SDK (2.11.) side */
      // query.exclude('owner')

      const object = await query.get(obj.id);
      expect(object.get('field')).toBe('field');
      expect(object.get('test')).toBe('test');
      expect(object.get('owner')).toBe(undefined);
      done();
    });
  });
});
