// This is a port of the test suite:
// hungry/js/test/parse_user_test.js
//
// Things that we didn't port:
// Tests that involve revocable sessions.
// Tests that involve sending password reset emails.

'use strict';

const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
const request = require('../lib/request');
const passwordCrypto = require('../lib/password');
const Config = require('../lib/Config');
const cryptoUtils = require('../lib/cryptoUtils');

describe('allowExpiredAuthDataToken option', () => {
  it('should accept true value', async () => {
    await reconfigureServer({ allowExpiredAuthDataToken: true });
    expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(true);
  });

  it('should accept false value', async () => {
    await reconfigureServer({ allowExpiredAuthDataToken: false });
    expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(false);
  });

  it('should default false', async () => {
    await reconfigureServer({});
    expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(false);
  });

  it('should enforce boolean values', async () => {
    const options = [[], 'a', '', 0, 1, {}, 'true', 'false'];
    for (const option of options) {
      await expectAsync(reconfigureServer({ allowExpiredAuthDataToken: option })).toBeRejected();
    }
  });
});

describe('Parse.User testing', () => {
  it('user sign up class method', async done => {
    const user = await Parse.User.signUp('asdf', 'zxcv');
    ok(user.getSessionToken());
    done();
  });

  it('user sign up instance method', async () => {
    const user = new Parse.User();
    user.setPassword('asdf');
    user.setUsername('zxcv');
    await user.signUp();
    ok(user.getSessionToken());
  });

  it('user login wrong username', async done => {
    await Parse.User.signUp('asdf', 'zxcv');
    try {
      await Parse.User.logIn('non_existent_user', 'asdf3');
      done.fail();
    } catch (e) {
      expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
      done();
    }
  });

  it('user login wrong password', async done => {
    await Parse.User.signUp('asdf', 'zxcv');
    try {
      await Parse.User.logIn('asdf', 'asdfWrong');
      done.fail();
    } catch (e) {
      expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
      done();
    }
  });

  it('user login with context', async () => {
    let hit = 0;
    const context = { foo: 'bar' };
    Parse.Cloud.beforeLogin(req => {
      expect(req.context).toEqual(context);
      hit++;
    });
    Parse.Cloud.afterLogin(req => {
      expect(req.context).toEqual(context);
      hit++;
    });
    await Parse.User.signUp('asdf', 'zxcv');
    await request({
      method: 'POST',
      url: 'http://localhost:8378/1/login',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'X-Parse-Cloud-Context': JSON.stringify(context),
        'Content-Type': 'application/json',
      },
      body: {
        _method: 'GET',
        username: 'asdf',
        password: 'zxcv',
      },
    });
    expect(hit).toBe(2);
  });

  it('user login with non-string username with REST API', async done => {
    await Parse.User.signUp('asdf', 'zxcv');
    request({
      method: 'POST',
      url: 'http://localhost:8378/1/login',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        _method: 'GET',
        username: { $regex: '^asd' },
        password: 'zxcv',
      },
    })
      .then(res => {
        fail(`no request should succeed: ${JSON.stringify(res)}`);
        done();
      })
      .catch(err => {
        expect(err.status).toBe(404);
        expect(err.text).toMatch(
          `{"code":${Parse.Error.OBJECT_NOT_FOUND},"error":"Invalid username/password."}`
        );
        done();
      });
  });

  it('user login with non-string username with REST API (again)', async done => {
    await Parse.User.signUp('asdf', 'zxcv');
    request({
      method: 'POST',
      url: 'http://localhost:8378/1/login',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        _method: 'GET',
        username: 'asdf',
        password: { $regex: '^zx' },
      },
    })
      .then(res => {
        fail(`no request should succeed: ${JSON.stringify(res)}`);
        done();
      })
      .catch(err => {
        expect(err.status).toBe(404);
        expect(err.text).toMatch(
          `{"code":${Parse.Error.OBJECT_NOT_FOUND},"error":"Invalid username/password."}`
        );
        done();
      });
  });

  it('user login using POST with REST API', async done => {
    await Parse.User.signUp('some_user', 'some_password');
    request({
      method: 'POST',
      url: 'http://localhost:8378/1/login',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
      },
      body: {
        username: 'some_user',
        password: 'some_password',
      },
    })
      .then(res => {
        expect(res.data.username).toBe('some_user');
        done();
      })
      .catch(err => {
        fail(`no request should fail: ${JSON.stringify(err)}`);
        done();
      });
  });

  it('user login', async done => {
    await Parse.User.signUp('asdf', 'zxcv');
    const user = await Parse.User.logIn('asdf', 'zxcv');
    equal(user.get('username'), 'asdf');
    const ACL = user.getACL();
    expect(ACL.getReadAccess(user)).toBe(true);
    expect(ACL.getWriteAccess(user)).toBe(true);
    expect(ACL.getPublicReadAccess()).toBe(false);
    expect(ACL.getPublicWriteAccess()).toBe(false);
    const perms = ACL.permissionsById;
    expect(Object.keys(perms).length).toBe(1);
    expect(perms[user.id].read).toBe(true);
    expect(perms[user.id].write).toBe(true);
    expect(perms['*']).toBeUndefined();
    done();
  });

  it('should respect ACL without locking user out', done => {
    const user = new Parse.User();
    const ACL = new Parse.ACL();
    ACL.setPublicReadAccess(false);
    ACL.setPublicWriteAccess(false);
    user.setUsername('asdf');
    user.setPassword('zxcv');
    user.setACL(ACL);
    user
      .signUp()
      .then(() => {
        return Parse.User.logIn('asdf', 'zxcv');
      })
      .then(user => {
        equal(user.get('username'), 'asdf');
        const ACL = user.getACL();
        expect(ACL.getReadAccess(user)).toBe(true);
        expect(ACL.getWriteAccess(user)).toBe(true);
        expect(ACL.getPublicReadAccess()).toBe(false);
        expect(ACL.getPublicWriteAccess()).toBe(false);
        const perms = ACL.permissionsById;
        expect(Object.keys(perms).length).toBe(1);
        expect(perms[user.id].read).toBe(true);
        expect(perms[user.id].write).toBe(true);
        expect(perms['*']).toBeUndefined();
        // Try to lock out user
        const newACL = new Parse.ACL();
        newACL.setReadAccess(user.id, false);
        newACL.setWriteAccess(user.id, false);
        user.setACL(newACL);
        return user.save();
      })
      .then(() => {
        return Parse.User.logIn('asdf', 'zxcv');
      })
      .then(user => {
        equal(user.get('username'), 'asdf');
        const ACL = user.getACL();
        expect(ACL.getReadAccess(user)).toBe(true);
        expect(ACL.getWriteAccess(user)).toBe(true);
        expect(ACL.getPublicReadAccess()).toBe(false);
        expect(ACL.getPublicWriteAccess()).toBe(false);
        const perms = ACL.permissionsById;
        expect(Object.keys(perms).length).toBe(1);
        expect(perms[user.id].read).toBe(true);
        expect(perms[user.id].write).toBe(true);
        expect(perms['*']).toBeUndefined();
        done();
      })
      .catch(() => {
        fail('Should not fail');
        done();
      });
  });

  it('should let masterKey lockout user', done => {
    const user = new Parse.User();
    const ACL = new Parse.ACL();
    ACL.setPublicReadAccess(false);
    ACL.setPublicWriteAccess(false);
    user.setUsername('asdf');
    user.setPassword('zxcv');
    user.setACL(ACL);
    user
      .signUp()
      .then(() => {
        return Parse.User.logIn('asdf', 'zxcv');
      })
      .then(user => {
        equal(user.get('username'), 'asdf');
        // Lock the user down
        const ACL = new Parse.ACL();
        user.setACL(ACL);
        return user.save(null, { useMasterKey: true });
      })
      .then(() => {
        expect(user.getACL().getPublicReadAccess()).toBe(false);
        return Parse.User.logIn('asdf', 'zxcv');
      })
      .then(done.fail)
      .catch(err => {
        expect(err.message).toBe('Invalid username/password.');
        expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
        done();
      });
  });

  it_only_db('mongo')('should let legacy users without ACL login', async () => {
    await reconfigureServer();
    const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
    const adapter = new MongoStorageAdapter({
      collectionPrefix: 'test_',
      uri: databaseURI,
    });
    await adapter.connect();
    await adapter.database.dropDatabase();
    delete adapter.connectionPromise;
    Config.get(Parse.applicationId).schemaCache.clear();

    const user = new Parse.User();
    await user.signUp({
      username: 'newUser',
      password: 'password',
    });

    const collection = await adapter._adaptiveCollection('_User');
    await collection.insertOne({
      // the hashed password is 'password' hashed
      _hashed_password: '$2b$10$mJ2ca2UbCM9hlojYHZxkQe8pyEXe5YMg0nMdvP4AJBeqlTEZJ6/Uu',
      _session_token: 'xxx',
      email: 'xxx@a.b',
      username: 'oldUser',
      emailVerified: true,
      _email_verify_token: 'yyy',
    });

    // get the 2 users
    const users = await collection.find();
    expect(users.length).toBe(2);

    const aUser = await Parse.User.logIn('oldUser', 'password');
    expect(aUser).not.toBeUndefined();

    const newUser = await Parse.User.logIn('newUser', 'password');
    expect(newUser).not.toBeUndefined();
  });

  it('should be let masterKey lock user out with authData', async () => {
    const response = await request({
      method: 'POST',
      url: 'http://localhost:8378/1/classes/_User',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        key: 'value',
        authData: { anonymous: { id: '00000000-0000-0000-0000-000000000001' } },
      },
    });
    const body = response.data;
    const objectId = body.objectId;
    const sessionToken = body.sessionToken;
    expect(sessionToken).toBeDefined();
    expect(objectId).toBeDefined();
    const user = new Parse.User();
    user.id = objectId;
    const ACL = new Parse.ACL();
    user.setACL(ACL);
    await user.save(null, { useMasterKey: true });
    // update the user
    const options = {
      method: 'POST',
      url: `http://localhost:8378/1/classes/_User/`,
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        key: 'otherValue',
        authData: {
          anonymous: { id: '00000000-0000-0000-0000-000000000001' },
        },
      },
    };
    const res = await request(options);
    expect(res.data.objectId).not.toEqual(objectId);
  });

  it('user login with files', done => {
    const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain');
    file
      .save()
      .then(file => {
        return Parse.User.signUp('asdf', 'zxcv', { file: file });
      })
      .then(() => {
        return Parse.User.logIn('asdf', 'zxcv');
      })
      .then(user => {
        const fileAgain = user.get('file');
        ok(fileAgain.name());
        ok(fileAgain.url());
        done();
      })
      .catch(err => {
        jfail(err);
        done();
      });
  });

  it('become sends token back', done => {
    let user = null;
    let sessionToken = null;

    Parse.User.signUp('Jason', 'Parse', { code: 'red' })
      .then(newUser => {
        user = newUser;
        expect(user.get('code'), 'red');

        sessionToken = newUser.getSessionToken();
        expect(sessionToken).toBeDefined();

        return Parse.User.become(sessionToken);
      })
      .then(newUser => {
        expect(newUser.id).toEqual(user.id);
        expect(newUser.get('username'), 'Jason');
        expect(newUser.get('code'), 'red');
        expect(newUser.getSessionToken()).toEqual(sessionToken);
      })
      .then(
        () => {
          done();
        },
        error => {
          jfail(error);
          done();
        }
      );
  });

  it('become', done => {
    let user = null;
    let sessionToken = null;

    Promise.resolve()
      .then(function () {
        return Parse.User.signUp('Jason', 'Parse', { code: 'red' });
      })
      .then(function (newUser) {
        equal(Parse.User.current(), newUser);

        user = newUser;
        sessionToken = newUser.getSessionToken();
        ok(sessionToken);

        return Parse.User.logOut();
      })
      .then(() => {
        ok(!Parse.User.current());

        return Parse.User.become(sessionToken);
      })
      .then(function (newUser) {
        equal(Parse.User.current(), newUser);

        ok(newUser);
        equal(newUser.id, user.id);
        equal(newUser.get('username'), 'Jason');
        equal(newUser.get('code'), 'red');

        return Parse.User.logOut();
      })
      .then(() => {
        ok(!Parse.User.current());

        return Parse.User.become('somegarbage');
      })
      .then(
        function () {
          // This should have failed actually.
          ok(false, "Shouldn't have been able to log in with garbage session token.");
        },
        function (error) {
          ok(error);
          // Handle the error.
          return Promise.resolve();
        }
      )
      .then(
        function () {
          done();
        },
        function (error) {
          ok(false, error);
          done();
        }
      );
  });

  it('should not call beforeLogin with become', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);

    let hit = 0;
    Parse.Cloud.beforeLogin(() => {
      hit++;
    });

    await Parse.User._logInWith('facebook');
    const sessionToken = Parse.User.current().getSessionToken();
    await Parse.User.become(sessionToken);
    expect(hit).toBe(0);
    done();
  });

  it('cannot save non-authed user', async done => {
    let user = new Parse.User();
    user.set({
      password: 'asdf',
      email: 'asdf@example.com',
      username: 'zxcv',
    });
    let userAgain = await user.signUp();
    equal(userAgain, user);
    const query = new Parse.Query(Parse.User);
    const userNotAuthed = await query.get(user.id);
    user = new Parse.User();
    user.set({
      username: 'hacker',
      password: 'password',
    });
    userAgain = await user.signUp();
    equal(userAgain, user);
    userNotAuthed.set('username', 'changed');
    userNotAuthed.save().then(fail, err => {
      expect(err.code).toEqual(Parse.Error.SESSION_MISSING);
      done();
    });
  });

  it('cannot delete non-authed user', async done => {
    let user = new Parse.User();
    await user.signUp({
      password: 'asdf',
      email: 'asdf@example.com',
      username: 'zxcv',
    });
    const query = new Parse.Query(Parse.User);
    const userNotAuthed = await query.get(user.id);
    user = new Parse.User();
    const userAgain = await user.signUp({
      username: 'hacker',
      password: 'password',
    });
    equal(userAgain, user);
    userNotAuthed.set('username', 'changed');
    try {
      await userNotAuthed.destroy();
      done.fail();
    } catch (e) {
      expect(e.code).toBe(Parse.Error.SESSION_MISSING);
      done();
    }
  });

  it('cannot saveAll with non-authed user', async done => {
    let user = new Parse.User();
    await user.signUp({
      password: 'asdf',
      email: 'asdf@example.com',
      username: 'zxcv',
    });
    const query = new Parse.Query(Parse.User);
    const userNotAuthed = await query.get(user.id);
    user = new Parse.User();
    await user.signUp({
      username: 'hacker',
      password: 'password',
    });
    const userNotAuthedNotChanged = await query.get(user.id);
    userNotAuthed.set('username', 'changed');
    const object = new TestObject();
    await object.save({
      user: userNotAuthedNotChanged,
    });
    const item1 = new TestObject();
    await item1.save({
      number: 0,
    });
    item1.set('number', 1);
    const item2 = new TestObject();
    item2.set('number', 2);
    try {
      await Parse.Object.saveAll([item1, item2, userNotAuthed]);
      done.fail();
    } catch (e) {
      expect(e.code).toBe(Parse.Error.SESSION_MISSING);
      done();
    }
  });

  it('never locks himself up', async () => {
    const user = new Parse.User();
    await user.signUp({
      username: 'username',
      password: 'password',
    });
    user.setACL(new Parse.ACL());
    await user.save();
    await user.fetch();
    expect(user.getACL().getReadAccess(user)).toBe(true);
    expect(user.getACL().getWriteAccess(user)).toBe(true);
    const publicReadACL = new Parse.ACL();
    publicReadACL.setPublicReadAccess(true);

    // Create an administrator role with a single admin user
    const role = new Parse.Role('admin', publicReadACL);
    const admin = new Parse.User();
    await admin.signUp({
      username: 'admin',
      password: 'admin',
    });
    role.getUsers().add(admin);
    await role.save(null, { useMasterKey: true });

    // Grant the admins write rights on the user
    const acl = user.getACL();
    acl.setRoleWriteAccess(role, true);
    acl.setRoleReadAccess(role, true);

    // Update with the masterKey just to be sure
    await user.save({ ACL: acl }, { useMasterKey: true });

    // Try to update from admin... should all work fine
    await user.save({ key: 'fromAdmin' }, { sessionToken: admin.getSessionToken() });
    await user.fetch();
    expect(user.toJSON().key).toEqual('fromAdmin');

    // Try to save when logged out (public)
    let failed = false;
    try {
      // Ensure no session token is sent
      await Parse.User.logOut();
      await user.save({ key: 'fromPublic' });
    } catch (e) {
      failed = true;
      expect(e.code).toBe(Parse.Error.SESSION_MISSING);
    }
    expect({ failed }).toEqual({ failed: true });

    // Try to save with a random user, should fail
    failed = false;
    const anyUser = new Parse.User();
    await anyUser.signUp({
      username: 'randomUser',
      password: 'password',
    });
    try {
      await user.save({ key: 'fromAnyUser' });
    } catch (e) {
      failed = true;
      expect(e.code).toBe(Parse.Error.SESSION_MISSING);
    }
    expect({ failed }).toEqual({ failed: true });
  });

  it('current user', done => {
    const user = new Parse.User();
    user.set('password', 'asdf');
    user.set('email', 'asdf@example.com');
    user.set('username', 'zxcv');
    user
      .signUp()
      .then(() => {
        const currentUser = Parse.User.current();
        equal(user.id, currentUser.id);
        ok(user.getSessionToken());

        const currentUserAgain = Parse.User.current();
        // should be the same object
        equal(currentUser, currentUserAgain);

        // test logging out the current user
        return Parse.User.logOut();
      })
      .then(() => {
        equal(Parse.User.current(), null);
        done();
      });
  });

  it('user.isCurrent', done => {
    const user1 = new Parse.User();
    const user2 = new Parse.User();
    const user3 = new Parse.User();

    user1.set('username', 'a');
    user2.set('username', 'b');
    user3.set('username', 'c');

    user1.set('password', 'password');
    user2.set('password', 'password');
    user3.set('password', 'password');

    user1
      .signUp()
      .then(() => {
        equal(user1.isCurrent(), true);
        equal(user2.isCurrent(), false);
        equal(user3.isCurrent(), false);
        return user2.signUp();
      })
      .then(() => {
        equal(user1.isCurrent(), false);
        equal(user2.isCurrent(), true);
        equal(user3.isCurrent(), false);
        return user3.signUp();
      })
      .then(() => {
        equal(user1.isCurrent(), false);
        equal(user2.isCurrent(), false);
        equal(user3.isCurrent(), true);
        return Parse.User.logIn('a', 'password');
      })
      .then(() => {
        equal(user1.isCurrent(), true);
        equal(user2.isCurrent(), false);
        equal(user3.isCurrent(), false);
        return Parse.User.logIn('b', 'password');
      })
      .then(() => {
        equal(user1.isCurrent(), false);
        equal(user2.isCurrent(), true);
        equal(user3.isCurrent(), false);
        return Parse.User.logIn('b', 'password');
      })
      .then(() => {
        equal(user1.isCurrent(), false);
        equal(user2.isCurrent(), true);
        equal(user3.isCurrent(), false);
        return Parse.User.logOut();
      })
      .then(() => {
        equal(user2.isCurrent(), false);
        done();
      });
  });

  it('user associations', async done => {
    const child = new TestObject();
    await child.save();
    const user = new Parse.User();
    user.set('password', 'asdf');
    user.set('email', 'asdf@example.com');
    user.set('username', 'zxcv');
    user.set('child', child);
    await user.signUp();
    const object = new TestObject();
    object.set('user', user);
    await object.save();
    const query = new Parse.Query(TestObject);
    const objectAgain = await query.get(object.id);
    const userAgain = objectAgain.get('user');
    await userAgain.fetch();
    equal(user.id, userAgain.id);
    equal(userAgain.get('child').id, child.id);
    done();
  });

  it('user queries', async done => {
    const user = new Parse.User();
    user.set('password', 'asdf');
    user.set('email', 'asdf@example.com');
    user.set('username', 'zxcv');
    await user.signUp();
    const query = new Parse.Query(Parse.User);
    const userAgain = await query.get(user.id);
    equal(userAgain.id, user.id);
    const users = await query.find();
    equal(users.length, 1);
    equal(users[0].id, user.id);
    ok(userAgain.get('email'), 'asdf@example.com');
    done();
  });

  function signUpAll(list, optionsOrCallback) {
    let promise = Promise.resolve();
    list.forEach(user => {
      promise = promise.then(function () {
        return user.signUp();
      });
    });
    promise = promise.then(function () {
      return list;
    });
    return promise.then(optionsOrCallback);
  }

  it('contained in user array queries', async done => {
    const USERS = 4;
    const MESSAGES = 5;

    // Make a list of users.
    const userList = range(USERS).map(function (i) {
      const user = new Parse.User();
      user.set('password', 'user_num_' + i);
      user.set('email', 'user_num_' + i + '@example.com');
      user.set('username', 'xinglblog_num_' + i);
      return user;
    });

    signUpAll(userList, async function (users) {
      // Make a list of messages.
      if (!users || users.length != USERS) {
        fail('signupAll failed');
        done();
        return;
      }
      const messageList = range(MESSAGES).map(function (i) {
        const message = new TestObject();
        message.set('to', users[(i + 1) % USERS]);
        message.set('from', users[i % USERS]);
        return message;
      });

      // Save all the messages.
      await Parse.Object.saveAll(messageList);

      // Assemble an "in" list.
      const inList = [users[0], users[3], users[3]]; // Intentional dupe
      const query = new Parse.Query(TestObject);
      query.containedIn('from', inList);
      const results = await query.find();
      equal(results.length, 3);
      done();
    });
  });

  it("saving a user signs them up but doesn't log them in", async done => {
    const user = new Parse.User();
    await user.save({
      password: 'asdf',
      email: 'asdf@example.com',
      username: 'zxcv',
    });
    equal(Parse.User.current(), null);
    done();
  });

  it('user updates', async done => {
    const user = new Parse.User();
    await user.signUp({
      password: 'asdf',
      email: 'asdf@example.com',
      username: 'zxcv',
    });

    user.set('username', 'test');
    await user.save();
    equal(Object.keys(user.attributes).length, 5);
    ok(user.attributes['username']);
    ok(user.attributes['email']);
    await user.destroy();
    const query = new Parse.Query(Parse.User);
    try {
      await query.get(user.id);
      done.fail();
    } catch (error) {
      // The user should no longer exist.
      equal(error.code, Parse.Error.OBJECT_NOT_FOUND);
      done();
    }
  });

  it('count users', async done => {
    const james = new Parse.User();
    james.set('username', 'james');
    james.set('password', 'mypass');
    await james.signUp();
    const kevin = new Parse.User();
    kevin.set('username', 'kevin');
    kevin.set('password', 'mypass');
    await kevin.signUp();
    const query = new Parse.Query(Parse.User);
    const count = await query.find({ useMasterKey: true });
    equal(count.length, 2);
    done();
  });

  it('user sign up with container class', async done => {
    await Parse.User.signUp('ilya', 'mypass', { array: ['hello'] });
    done();
  });

  it('user modified while saving', async done => {
    Parse.Object.disableSingleInstance();
    await reconfigureServer();
    const user = new Parse.User();
    user.set('username', 'alice');
    user.set('password', 'password');
    user.signUp().then(function (userAgain) {
      equal(userAgain.get('username'), 'bob');
      ok(userAgain.dirty('username'));
      const query = new Parse.Query(Parse.User);
      query.get(user.id).then(freshUser => {
        equal(freshUser.id, user.id);
        equal(freshUser.get('username'), 'alice');
        done();
      });
    });
    // Jump a frame so the signup call is properly sent
    // This is due to the fact that now, we use real promises
    process.nextTick(() => {
      ok(user.set('username', 'bob'));
    });
  });

  it('user modified while saving with unsaved child', done => {
    Parse.Object.disableSingleInstance();
    const user = new Parse.User();
    user.set('username', 'alice');
    user.set('password', 'password');
    user.set('child', new TestObject());
    user.signUp().then(userAgain => {
      equal(userAgain.get('username'), 'bob');
      // Should be dirty, but it depends on batch support.
      // ok(userAgain.dirty("username"));
      const query = new Parse.Query(Parse.User);
      query.get(user.id).then(freshUser => {
        equal(freshUser.id, user.id);
        // Should be alice, but it depends on batch support.
        equal(freshUser.get('username'), 'bob');
        done();
      });
    });
    ok(user.set('username', 'bob'));
  });

  it('user loaded from localStorage from signup', async done => {
    const alice = await Parse.User.signUp('alice', 'password');
    ok(alice.id, 'Alice should have an objectId');
    ok(alice.getSessionToken(), 'Alice should have a session token');
    equal(alice.get('password'), undefined, 'Alice should not have a password');

    // Simulate the environment getting reset.
    Parse.User._currentUser = null;
    Parse.User._currentUserMatchesDisk = false;

    const aliceAgain = Parse.User.current();
    equal(aliceAgain.get('username'), 'alice');
    equal(aliceAgain.id, alice.id, 'currentUser should have objectId');
    ok(aliceAgain.getSessionToken(), 'currentUser should have a sessionToken');
    equal(alice.get('password'), undefined, 'currentUser should not have password');
    done();
  });

  it('user loaded from localStorage from login', done => {
    let id;
    Parse.User.signUp('alice', 'password')
      .then(alice => {
        id = alice.id;
        return Parse.User.logOut();
      })
      .then(() => {
        return Parse.User.logIn('alice', 'password');
      })
      .then(() => {
        // Force the current user to read from disk
        delete Parse.User._currentUser;
        delete Parse.User._currentUserMatchesDisk;

        const userFromDisk = Parse.User.current();
        equal(userFromDisk.get('password'), undefined, 'password should not be in attributes');
        equal(userFromDisk.id, id, 'id should be set');
        ok(userFromDisk.getSessionToken(), 'currentUser should have a sessionToken');
        done();
      });
  });

  it('saving user after browser refresh', done => {
    let id;

    Parse.User.signUp('alice', 'password', null)
      .then(function (alice) {
        id = alice.id;
        return Parse.User.logOut();
      })
      .then(() => {
        return Parse.User.logIn('alice', 'password');
      })
      .then(function () {
        // Simulate browser refresh by force-reloading user from localStorage
        Parse.User._clearCache();

        // Test that this save works correctly
        return Parse.User.current().save({ some_field: 1 });
      })
      .then(
        function () {
          // Check the user in memory just after save operation
          const userInMemory = Parse.User.current();

          equal(
            userInMemory.getUsername(),
            'alice',
            'saving user should not remove existing fields'
          );

          equal(userInMemory.get('some_field'), 1, 'saving user should save specified field');

          equal(
            userInMemory.get('password'),
            undefined,
            'password should not be in attributes after saving user'
          );

          equal(
            userInMemory.get('objectId'),
            undefined,
            'objectId should not be in attributes after saving user'
          );

          equal(
            userInMemory.get('_id'),
            undefined,
            '_id should not be in attributes after saving user'
          );

          equal(userInMemory.id, id, 'id should be set');

          expect(userInMemory.updatedAt instanceof Date).toBe(true);

          ok(userInMemory.createdAt instanceof Date);

          ok(userInMemory.getSessionToken(), 'user should have a sessionToken after saving');

          // Force the current user to read from localStorage, and check again
          delete Parse.User._currentUser;
          delete Parse.User._currentUserMatchesDisk;
          const userFromDisk = Parse.User.current();

          equal(
            userFromDisk.getUsername(),
            'alice',
            'userFromDisk should have previously existing fields'
          );

          equal(userFromDisk.get('some_field'), 1, 'userFromDisk should have saved field');

          equal(
            userFromDisk.get('password'),
            undefined,
            'password should not be in attributes of userFromDisk'
          );

          equal(
            userFromDisk.get('objectId'),
            undefined,
            'objectId should not be in attributes of userFromDisk'
          );

          equal(
            userFromDisk.get('_id'),
            undefined,
            '_id should not be in attributes of userFromDisk'
          );

          equal(userFromDisk.id, id, 'id should be set on userFromDisk');

          ok(userFromDisk.updatedAt instanceof Date);

          ok(userFromDisk.createdAt instanceof Date);

          ok(userFromDisk.getSessionToken(), 'userFromDisk should have a sessionToken');

          done();
        },
        function (error) {
          ok(false, error);
          done();
        }
      );
  });

  it('user with missing username', async done => {
    const user = new Parse.User();
    user.set('password', 'foo');
    try {
      await user.signUp();
      done.fail();
    } catch (error) {
      equal(error.code, Parse.Error.OTHER_CAUSE);
      done();
    }
  });

  it('user with missing password', async done => {
    const user = new Parse.User();
    user.set('username', 'foo');
    try {
      await user.signUp();
      done.fail();
    } catch (error) {
      equal(error.code, Parse.Error.OTHER_CAUSE);
      done();
    }
  });

  it('user stupid subclassing', async done => {
    const SuperUser = Parse.Object.extend('User');
    const user = new SuperUser();
    user.set('username', 'bob');
    user.set('password', 'welcome');
    ok(user instanceof Parse.User, 'Subclassing User should have worked');
    await user.signUp();
    done();
  });

  it('user signup class method uses subclassing', async done => {
    const SuperUser = Parse.User.extend({
      secret: function () {
        return 1337;
      },
    });

    const user = await Parse.User.signUp('bob', 'welcome');
    ok(user instanceof SuperUser, 'Subclassing User should have worked');
    equal(user.secret(), 1337);
    done();
  });

  it('user on disk gets updated after save', async done => {
    Parse.User.extend({
      isSuper: function () {
        return true;
      },
    });

    const user = await Parse.User.signUp('bob', 'welcome');
    await user.save('secret', 1337);
    delete Parse.User._currentUser;
    delete Parse.User._currentUserMatchesDisk;

    const userFromDisk = Parse.User.current();
    equal(userFromDisk.get('secret'), 1337);
    ok(userFromDisk.isSuper(), 'The subclass should have been used');
    done();
  });

  it("current user isn't dirty", async done => {
    const user = await Parse.User.signUp('andrew', 'oppa', {
      style: 'gangnam',
    });
    ok(!user.dirty('style'), 'The user just signed up.');
    Parse.User._currentUser = null;
    Parse.User._currentUserMatchesDisk = false;
    const userAgain = Parse.User.current();
    ok(!userAgain.dirty('style'), 'The user was just read from disk.');
    done();
  });

  const getMockFacebookProviderWithIdToken = function (id, token) {
    return {
      authData: {
        id: id,
        access_token: token,
        expiration_date: new Date().toJSON(),
      },
      shouldError: false,
      loggedOut: false,
      synchronizedUserId: null,
      synchronizedAuthToken: null,
      synchronizedExpiration: null,

      authenticate: function (options) {
        if (this.shouldError) {
          options.error(this, 'An error occurred');
        } else if (this.shouldCancel) {
          options.error(this, null);
        } else {
          options.success(this, this.authData);
        }
      },
      restoreAuthentication: function (authData) {
        if (!authData) {
          this.synchronizedUserId = null;
          this.synchronizedAuthToken = null;
          this.synchronizedExpiration = null;
          return true;
        }
        this.synchronizedUserId = authData.id;
        this.synchronizedAuthToken = authData.access_token;
        this.synchronizedExpiration = authData.expiration_date;
        return true;
      },
      getAuthType() {
        return 'facebook';
      },
      deauthenticate: function () {
        this.loggedOut = true;
        this.restoreAuthentication(null);
      },
    };
  };

  // Note that this mocks out client-side Facebook action rather than
  // server-side.
  const getMockFacebookProvider = function () {
    return getMockFacebookProviderWithIdToken('8675309', 'jenny');
  };

  const getMockMyOauthProvider = function () {
    return {
      authData: {
        id: '12345',
        access_token: '12345',
        expiration_date: new Date().toJSON(),
      },
      shouldError: false,
      loggedOut: false,
      synchronizedUserId: null,
      synchronizedAuthToken: null,
      synchronizedExpiration: null,

      authenticate(options) {
        if (this.shouldError) {
          options.error(this, 'An error occurred');
        } else if (this.shouldCancel) {
          options.error(this, null);
        } else {
          options.success(this, this.authData);
        }
      },
      restoreAuthentication(authData) {
        if (!authData) {
          this.synchronizedUserId = null;
          this.synchronizedAuthToken = null;
          this.synchronizedExpiration = null;
          return true;
        }
        this.synchronizedUserId = authData.id;
        this.synchronizedAuthToken = authData.access_token;
        this.synchronizedExpiration = authData.expiration_date;
        return true;
      },
      getAuthType() {
        return 'myoauth';
      },
      deauthenticate() {
        this.loggedOut = true;
        this.restoreAuthentication(null);
      },
    };
  };

  Parse.User.extend({
    extended: function () {
      return true;
    },
  });

  it('log in with provider', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User');
    strictEqual(Parse.User.current(), model);
    ok(model.extended(), 'Should have used subclass.');
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked to facebook');
    done();
  });

  it('can not set authdata to null', async () => {
    try {
      const provider = getMockFacebookProvider();
      Parse.User._registerAuthenticationProvider(provider);
      const user = await Parse.User._logInWith('facebook');
      user.set('authData', null);
      await user.save();
      fail();
    } catch (e) {
      expect(e.message).toBe('This authentication method is unsupported.');
    }
  });

  it('ignore setting authdata to undefined', async () => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const user = await Parse.User._logInWith('facebook');
    user.set('authData', undefined);
    await user.save();
    let authData = user.get('authData');
    expect(authData).toBe(undefined);
    await user.fetch();
    authData = user.get('authData');
    expect(authData.facebook.id).toBeDefined();
  });

  it('user authData should be available in cloudcode (#2342)', async done => {
    Parse.Cloud.define('checkLogin', req => {
      expect(req.user).not.toBeUndefined();
      expect(Parse.FacebookUtils.isLinked(req.user)).toBe(true);
      return 'ok';
    });

    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User');
    strictEqual(Parse.User.current(), model);
    ok(model.extended(), 'Should have used subclass.');
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked to facebook');

    Parse.Cloud.run('checkLogin').then(done, done);
  });

  it('log in with provider and update token', async done => {
    const provider = getMockFacebookProvider();
    const secondProvider = getMockFacebookProviderWithIdToken('8675309', 'jenny_valid_token');
    Parse.User._registerAuthenticationProvider(provider);
    await Parse.User._logInWith('facebook');
    Parse.User._registerAuthenticationProvider(secondProvider);
    await Parse.User.logOut();
    await Parse.User._logInWith('facebook');
    expect(secondProvider.synchronizedAuthToken).toEqual('jenny_valid_token');
    // Make sure we can login with the new token again
    await Parse.User.logOut();
    await Parse.User._logInWith('facebook');
    done();
  });

  it('returns authData when authed and logged in with provider (regression test for #1498)', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const user = await Parse.User._logInWith('facebook');
    const userQuery = new Parse.Query(Parse.User);
    userQuery.get(user.id).then(user => {
      expect(user.get('authData')).not.toBeUndefined();
      done();
    });
  });

  it('only creates a single session for an installation / user pair (#2885)', async done => {
    Parse.Object.disableSingleInstance();
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    await Parse.User.logInWith('facebook');
    await Parse.User.logInWith('facebook');
    const user = await Parse.User.logInWith('facebook');
    const sessionToken = user.getSessionToken();
    const query = new Parse.Query('_Session');
    return query
      .find({ useMasterKey: true })
      .then(results => {
        expect(results.length).toBe(1);
        expect(results[0].get('sessionToken')).toBe(sessionToken);
        expect(results[0].get('createdWith')).toEqual({
          action: 'login',
          authProvider: 'facebook',
        });
        done();
      })
      .catch(done.fail);
  });

  it('log in with provider with files', done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain');
    file
      .save()
      .then(file => {
        const user = new Parse.User();
        user.set('file', file);
        return user._linkWith('facebook', {});
      })
      .then(user => {
        expect(user._isLinked('facebook')).toBeTruthy();
        return Parse.User._logInWith('facebook', {});
      })
      .then(user => {
        const fileAgain = user.get('file');
        expect(fileAgain.name()).toMatch(/yolo.txt$/);
        expect(fileAgain.url()).toMatch(/yolo.txt$/);
      })
      .then(() => {
        done();
      })
      .catch(done.fail);
  });

  it('log in with provider twice', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User');
    strictEqual(Parse.User.current(), model);
    ok(model.extended(), 'Should have used the subclass.');
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked to facebook');

    Parse.User.logOut().then(async () => {
      ok(provider.loggedOut);
      provider.loggedOut = false;
      const innerModel = await Parse.User._logInWith('facebook');
      ok(innerModel instanceof Parse.User, 'Model should be a Parse.User');
      ok(innerModel === Parse.User.current(), 'Returned model should be the current user');
      ok(provider.authData.id === provider.synchronizedUserId);
      ok(provider.authData.access_token === provider.synchronizedAuthToken);
      ok(innerModel._isLinked('facebook'), 'User should be linked to facebook');
      ok(innerModel.existed(), 'User should not be newly-created');
      done();
    }, done.fail);
  });

  it('log in with provider failed', async done => {
    const provider = getMockFacebookProvider();
    provider.shouldError = true;
    Parse.User._registerAuthenticationProvider(provider);
    try {
      await Parse.User._logInWith('facebook');
      done.fail();
    } catch (error) {
      ok(error, 'Error should be non-null');
      done();
    }
  });

  it('log in with provider cancelled', async done => {
    const provider = getMockFacebookProvider();
    provider.shouldCancel = true;
    Parse.User._registerAuthenticationProvider(provider);
    try {
      await Parse.User._logInWith('facebook');
      done.fail();
    } catch (error) {
      ok(error === null, 'Error should be null');
      done();
    }
  });

  it('login with provider should not call beforeSave trigger', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    await Parse.User._logInWith('facebook');
    Parse.User.logOut().then(async () => {
      Parse.Cloud.beforeSave(Parse.User, function (req, res) {
        res.error("Before save shouldn't be called on login");
      });
      await Parse.User._logInWith('facebook');
      done();
    });
  });

  it('signup with provider should not call beforeLogin trigger', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);

    let hit = 0;
    Parse.Cloud.beforeLogin(() => {
      hit++;
    });

    await Parse.User._logInWith('facebook');
    expect(hit).toBe(0);
    done();
  });

  it('login with provider should call beforeLogin trigger', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);

    let hit = 0;
    Parse.Cloud.beforeLogin(req => {
      hit++;
      expect(req.object.get('authData')).toBeDefined();
      expect(req.object.get('name')).toBe('tupac shakur');
    });
    await Parse.User._logInWith('facebook');
    await Parse.User.current().save({ name: 'tupac shakur' });
    await Parse.User.logOut();
    await Parse.User._logInWith('facebook');
    expect(hit).toBe(1);
    done();
  });

  it('incorrect login with provider should not call beforeLogin trigger', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);

    let hit = 0;
    Parse.Cloud.beforeLogin(() => {
      hit++;
    });
    await Parse.User._logInWith('facebook');
    await Parse.User.logOut();
    provider.shouldError = true;
    try {
      await Parse.User._logInWith('facebook');
    } catch (e) {
      expect(e).toBeDefined();
    }
    expect(hit).toBe(0);
    done();
  });

  it('login with provider should be blockable by beforeLogin', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);

    let hit = 0;
    Parse.Cloud.beforeLogin(req => {
      hit++;
      if (req.object.get('isBanned')) {
        throw new Error('banned account');
      }
    });
    await Parse.User._logInWith('facebook');
    await Parse.User.current().save({ isBanned: true });
    await Parse.User.logOut();

    try {
      await Parse.User._logInWith('facebook');
      throw new Error('should not have continued login.');
    } catch (e) {
      expect(e.message).toBe('banned account');
    }

    expect(hit).toBe(1);
    done();
  });

  it('login with provider should be blockable by beforeLogin even when the user has a attached file', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);

    let hit = 0;
    Parse.Cloud.beforeLogin(req => {
      hit++;
      if (req.object.get('isBanned')) {
        throw new Error('banned account');
      }
    });

    const user = await Parse.User._logInWith('facebook');
    const base64 = 'aHR0cHM6Ly9naXRodWIuY29tL2t2bmt1YW5n';
    const file = new Parse.File('myfile.txt', { base64 });
    await file.save();
    await user.save({ isBanned: true, file });
    await Parse.User.logOut();

    try {
      await Parse.User._logInWith('facebook');
      throw new Error('should not have continued login.');
    } catch (e) {
      expect(e.message).toBe('banned account');
    }

    expect(hit).toBe(1);
    done();
  });

  it('logout with provider should call afterLogout trigger', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);

    let userId;
    Parse.Cloud.afterLogout(req => {
      expect(req.object.className).toEqual('_Session');
      expect(req.object.id).toBeDefined();
      const user = req.object.get('user');
      expect(user).toBeDefined();
      userId = user.id;
    });
    const user = await Parse.User._logInWith('facebook');
    await Parse.User.logOut();
    expect(user.id).toBe(userId);
    done();
  });

  it('link with provider', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const user = new Parse.User();
    user.set('username', 'testLinkWithProvider');
    user.set('password', 'mypass');
    await user.signUp();
    const model = await user._linkWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User');
    strictEqual(Parse.User.current(), model);
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked');
    done();
  });

  // What this means is, only one Parse User can be linked to a
  // particular Facebook account.
  it('link with provider for already linked user', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const user = new Parse.User();
    user.set('username', 'testLinkWithProviderToAlreadyLinkedUser');
    user.set('password', 'mypass');
    await user.signUp();
    const model = await user._linkWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User');
    strictEqual(Parse.User.current(), model);
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked.');
    const user2 = new Parse.User();
    user2.set('username', 'testLinkWithProviderToAlreadyLinkedUser2');
    user2.set('password', 'mypass');
    await user2.signUp();
    try {
      await user2._linkWith('facebook');
      done.fail();
    } catch (error) {
      expect(error.code).toEqual(Parse.Error.ACCOUNT_ALREADY_LINKED);
      done();
    }
  });

  it('link with provider should return sessionToken', async () => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const user = new Parse.User();
    user.set('username', 'testLinkWithProvider');
    user.set('password', 'mypass');
    await user.signUp();
    const query = new Parse.Query(Parse.User);
    const u2 = await query.get(user.id);
    const model = await u2._linkWith('facebook', {}, { useMasterKey: true });
    expect(u2.getSessionToken()).toBeDefined();
    expect(model.getSessionToken()).toBeDefined();
    expect(u2.getSessionToken()).toBe(model.getSessionToken());
  });

  it('link with provider via sessionToken should not create new sessionToken (Regression #5799)', async () => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const user = new Parse.User();
    user.set('username', 'testLinkWithProviderNoOverride');
    user.set('password', 'mypass');
    await user.signUp();
    const sessionToken = user.getSessionToken();

    await user._linkWith('facebook', {}, { sessionToken });
    expect(sessionToken).toBe(user.getSessionToken());

    expect(user._isLinked(provider)).toBe(true);
    await user._unlinkFrom(provider, { sessionToken });
    expect(user._isLinked(provider)).toBe(false);

    const become = await Parse.User.become(sessionToken);
    expect(sessionToken).toBe(become.getSessionToken());
  });

  it('link with provider failed', async done => {
    const provider = getMockFacebookProvider();
    provider.shouldError = true;
    Parse.User._registerAuthenticationProvider(provider);
    const user = new Parse.User();
    user.set('username', 'testLinkWithProvider');
    user.set('password', 'mypass');
    await user.signUp();
    try {
      await user._linkWith('facebook');
      done.fail();
    } catch (error) {
      ok(error, 'Linking should fail');
      ok(!user._isLinked('facebook'), 'User should not be linked to facebook');
      done();
    }
  });

  it('link with provider cancelled', async done => {
    const provider = getMockFacebookProvider();
    provider.shouldCancel = true;
    Parse.User._registerAuthenticationProvider(provider);
    const user = new Parse.User();
    user.set('username', 'testLinkWithProvider');
    user.set('password', 'mypass');
    await user.signUp();
    try {
      await user._linkWith('facebook');
      done.fail();
    } catch (error) {
      ok(!error, 'Linking should be cancelled');
      ok(!user._isLinked('facebook'), 'User should not be linked to facebook');
      done();
    }
  });

  it('unlink with provider', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User.');
    strictEqual(Parse.User.current(), model);
    ok(model.extended(), 'Should have used the subclass.');
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked to facebook.');
    await model._unlinkFrom('facebook');
    ok(!model._isLinked('facebook'), 'User should not be linked.');
    ok(!provider.synchronizedUserId, 'User id should be cleared.');
    ok(!provider.synchronizedAuthToken, 'Auth token should be cleared.');
    ok(!provider.synchronizedExpiration, 'Expiration should be cleared.');
    done();
  });

  it('unlink and link', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User');
    strictEqual(Parse.User.current(), model);
    ok(model.extended(), 'Should have used the subclass.');
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked to facebook');

    await model._unlinkFrom('facebook');
    ok(!model._isLinked('facebook'), 'User should not be linked to facebook');
    ok(!provider.synchronizedUserId, 'User id should be cleared');
    ok(!provider.synchronizedAuthToken, 'Auth token should be cleared');
    ok(!provider.synchronizedExpiration, 'Expiration should be cleared');

    await model._linkWith('facebook');
    ok(provider.synchronizedUserId, 'User id should have a value');
    ok(provider.synchronizedAuthToken, 'Auth token should have a value');
    ok(provider.synchronizedExpiration, 'Expiration should have a value');
    ok(model._isLinked('facebook'), 'User should be linked to facebook');
    done();
  });

  it('link multiple providers', async done => {
    const provider = getMockFacebookProvider();
    const mockProvider = getMockMyOauthProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User');
    strictEqual(Parse.User.current(), model);
    ok(model.extended(), 'Should have used the subclass.');
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked to facebook');
    Parse.User._registerAuthenticationProvider(mockProvider);
    const objectId = model.id;
    await model._linkWith('myoauth');
    expect(model.id).toEqual(objectId);
    ok(model._isLinked('facebook'), 'User should be linked to facebook');
    ok(model._isLinked('myoauth'), 'User should be linked to myoauth');
    done();
  });

  it('link multiple providers and updates token', async done => {
    const provider = getMockFacebookProvider();
    const secondProvider = getMockFacebookProviderWithIdToken('8675309', 'jenny_valid_token');

    const mockProvider = getMockMyOauthProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    Parse.User._registerAuthenticationProvider(mockProvider);
    const objectId = model.id;
    await model._linkWith('myoauth');
    Parse.User._registerAuthenticationProvider(secondProvider);
    await Parse.User.logOut();
    await Parse.User._logInWith('facebook');
    await Parse.User.logOut();
    const user = await Parse.User._logInWith('myoauth');
    expect(user.id).toBe(objectId);
    done();
  });

  it('link multiple providers and update token', async done => {
    const provider = getMockFacebookProvider();
    const mockProvider = getMockMyOauthProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    ok(model instanceof Parse.User, 'Model should be a Parse.User');
    strictEqual(Parse.User.current(), model);
    ok(model.extended(), 'Should have used the subclass.');
    strictEqual(provider.authData.id, provider.synchronizedUserId);
    strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
    strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
    ok(model._isLinked('facebook'), 'User should be linked to facebook');
    Parse.User._registerAuthenticationProvider(mockProvider);
    const objectId = model.id;
    await model._linkWith('myoauth');
    expect(model.id).toEqual(objectId);
    ok(model._isLinked('facebook'), 'User should be linked to facebook');
    ok(model._isLinked('myoauth'), 'User should be linked to myoauth');
    await model._linkWith('facebook');
    ok(model._isLinked('facebook'), 'User should be linked to facebook');
    ok(model._isLinked('myoauth'), 'User should be linked to myoauth');
    done();
  });

  it('should fail linking with existing', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    await Parse.User._logInWith('facebook');
    await Parse.User.logOut();
    const user = new Parse.User();
    user.setUsername('user');
    user.setPassword('password');
    await user.signUp();
    // try to link here
    try {
      await user._linkWith('facebook');
      done.fail();
    } catch (e) {
      done();
    }
  });

  it('should fail linking with existing through REST', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const model = await Parse.User._logInWith('facebook');
    const userId = model.id;
    Parse.User.logOut().then(() => {
      request({
        method: 'POST',
        url: Parse.serverURL + '/classes/_User',
        headers: {
          'X-Parse-Application-Id': Parse.applicationId,
          'X-Parse-REST-API-Key': 'rest',
          'Content-Type': 'application/json',
        },
        body: { authData: { facebook: provider.authData } },
      }).then(response => {
        const body = response.data;
        // make sure the location header is properly set
        expect(userId).not.toBeUndefined();
        expect(body.objectId).toEqual(userId);
        expect(response.headers.location).toEqual(Parse.serverURL + '/users/' + userId);
        done();
      });
    });
  });

  it('should not allow login with expired authData token since allowExpiredAuthDataToken is set to false by default', async () => {
    const provider = {
      authData: {
        id: '12345',
        access_token: 'token',
      },
      restoreAuthentication: function () {
        return true;
      },
      deauthenticate: function () {
        provider.authData = {};
      },
      authenticate: function (options) {
        options.success(this, provider.authData);
      },
      getAuthType: function () {
        return 'shortLivedAuth';
      },
    };
    defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('token');
    Parse.User._registerAuthenticationProvider(provider);
    await Parse.User._logInWith('shortLivedAuth', {});
    // Simulate a remotely expired token (like a short lived one)
    // In this case, we want success as it was valid once.
    // If the client needs an updated token, do lock the user out
    defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
    await expectAsync(Parse.User._logInWith('shortLivedAuth', {})).toBeRejected();
  });

  it('should allow PUT request with stale auth Data', done => {
    const provider = {
      authData: {
        id: '12345',
        access_token: 'token',
      },
      restoreAuthentication: function () {
        return true;
      },
      deauthenticate: function () {
        provider.authData = {};
      },
      authenticate: function (options) {
        options.success(this, provider.authData);
      },
      getAuthType: function () {
        return 'shortLivedAuth';
      },
    };
    defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('token');
    Parse.User._registerAuthenticationProvider(provider);
    Parse.User._logInWith('shortLivedAuth', {})
      .then(() => {
        // Simulate a remotely expired token (like a short lived one)
        // In this case, we want success as it was valid once.
        // If the client needs an updated one, do lock the user out
        defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
        return request({
          method: 'PUT',
          url: Parse.serverURL + '/users/' + Parse.User.current().id,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-Javascript-Key': Parse.javaScriptKey,
            'X-Parse-Session-Token': Parse.User.current().getSessionToken(),
            'Content-Type': 'application/json',
          },
          body: {
            key: 'value', // update a key
            authData: {
              // pass the original auth data
              shortLivedAuth: {
                id: '12345',
                access_token: 'token',
              },
            },
          },
        });
      })
      .then(
        () => {
          done();
        },
        err => {
          done.fail(err);
        }
      );
  });

  it('should properly error when password is missing', async done => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    const user = await Parse.User._logInWith('facebook');
    user.set('username', 'myUser');
    user.set('email', 'foo@example.com');
    user
      .save()
      .then(() => {
        return Parse.User.logOut();
      })
      .then(() => {
        return Parse.User.logIn('myUser', 'password');
      })
      .then(
        () => {
          fail('should not succeed');
          done();
        },
        err => {
          expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
          expect(err.message).toEqual('Invalid username/password.');
          done();
        }
      );
  });

  it('should have authData in beforeSave and afterSave', async done => {
    Parse.Cloud.beforeSave('_User', request => {
      const authData = request.object.get('authData');
      expect(authData).not.toBeUndefined();
      if (authData) {
        expect(authData.facebook.id).toEqual('8675309');
        expect(authData.facebook.access_token).toEqual('jenny');
      } else {
        fail('authData should be set');
      }
    });

    Parse.Cloud.afterSave('_User', request => {
      const authData = request.object.get('authData');
      expect(authData).not.toBeUndefined();
      if (authData) {
        expect(authData.facebook.id).toEqual('8675309');
        expect(authData.facebook.access_token).toEqual('jenny');
      } else {
        fail('authData should be set');
      }
    });

    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    await Parse.User._logInWith('facebook');
    done();
  });

  it('set password then change password', done => {
    Parse.User.signUp('bob', 'barker')
      .then(bob => {
        bob.setPassword('meower');
        return bob.save();
      })
      .then(() => {
        return Parse.User.logIn('bob', 'meower');
      })
      .then(
        bob => {
          expect(bob.getUsername()).toEqual('bob');
          done();
        },
        e => {
          console.log(e);
          fail();
        }
      );
  });

  it('authenticated check', async done => {
    const user = new Parse.User();
    user.set('username', 'darkhelmet');
    user.set('password', 'onetwothreefour');
    ok(!user.authenticated());
    await user.signUp(null);
    ok(user.authenticated());
    done();
  });

  it('log in with explicit facebook auth data', async done => {
    await Parse.FacebookUtils.logIn({
      id: '8675309',
      access_token: 'jenny',
      expiration_date: new Date().toJSON(),
    });
    done();
  });

  it('log in async with explicit facebook auth data', done => {
    Parse.FacebookUtils.logIn({
      id: '8675309',
      access_token: 'jenny',
      expiration_date: new Date().toJSON(),
    }).then(
      function () {
        done();
      },
      function (error) {
        ok(false, error);
        done();
      }
    );
  });

  it('link with explicit facebook auth data', async done => {
    const user = await Parse.User.signUp('mask', 'open sesame');
    Parse.FacebookUtils.link(user, {
      id: '8675309',
      access_token: 'jenny',
      expiration_date: new Date().toJSON(),
    }).then(done, error => {
      jfail(error);
      done();
    });
  });

  it('link async with explicit facebook auth data', async done => {
    const user = await Parse.User.signUp('mask', 'open sesame');
    Parse.FacebookUtils.link(user, {
      id: '8675309',
      access_token: 'jenny',
      expiration_date: new Date().toJSON(),
    }).then(
      function () {
        done();
      },
      function (error) {
        ok(false, error);
        done();
      }
    );
  });

  it('async methods', done => {
    const data = { foo: 'bar' };

    Parse.User.signUp('finn', 'human', data)
      .then(function (user) {
        equal(Parse.User.current(), user);
        equal(user.get('foo'), 'bar');
        return Parse.User.logOut();
      })
      .then(function () {
        return Parse.User.logIn('finn', 'human');
      })
      .then(function (user) {
        equal(user, Parse.User.current());
        equal(user.get('foo'), 'bar');
        return Parse.User.logOut();
      })
      .then(function () {
        const user = new Parse.User();
        user.set('username', 'jake');
        user.set('password', 'dog');
        user.set('foo', 'baz');
        return user.signUp();
      })
      .then(function (user) {
        equal(user, Parse.User.current());
        equal(user.get('foo'), 'baz');
        user = new Parse.User();
        user.set('username', 'jake');
        user.set('password', 'dog');
        return user.logIn();
      })
      .then(function (user) {
        equal(user, Parse.User.current());
        equal(user.get('foo'), 'baz');
        const userAgain = new Parse.User();
        userAgain.id = user.id;
        return userAgain.fetch();
      })
      .then(function (userAgain) {
        equal(userAgain.get('foo'), 'baz');
        done();
      });
  });

  it("querying for users doesn't get session tokens", done => {
    const user = new Parse.User();
    user.set('username', 'finn');
    user.set('password', 'human');
    user.set('foo', 'bar');
    const acl = new Parse.ACL();
    acl.setPublicReadAccess(true);
    user.setACL(acl);
    user
      .signUp()
      .then(function () {
        return Parse.User.logOut();
      })
      .then(() => {
        const user = new Parse.User();
        user.set('username', 'jake');
        user.set('password', 'dog');
        user.set('foo', 'baz');
        const acl = new Parse.ACL();
        acl.setPublicReadAccess(true);
        user.setACL(acl);
        return user.signUp();
      })
      .then(function () {
        return Parse.User.logOut();
      })
      .then(() => {
        const query = new Parse.Query(Parse.User);
        return query.find({ sessionToken: null });
      })
      .then(
        function (users) {
          equal(users.length, 2);
          users.forEach(user => {
            expect(user.getSessionToken()).toBeUndefined();
            ok(!user.getSessionToken(), 'user should not have a session token.');
          });
          done();
        },
        function (error) {
          ok(false, error);
          done();
        }
      );
  });

  it('querying for users only gets the expected fields', done => {
    const user = new Parse.User();
    user.setUsername('finn');
    user.setPassword('human');
    user.set('foo', 'bar');
    const acl = new Parse.ACL();
    acl.setPublicReadAccess(true);
    user.setACL(acl);
    user.signUp().then(() => {
      request({
        headers: {
          'X-Parse-Application-Id': 'test',
          'X-Parse-REST-API-Key': 'rest',
        },
        url: 'http://localhost:8378/1/users',
      }).then(response => {
        const b = response.data;
        expect(b.results.length).toEqual(1);
        const user = b.results[0];
        expect(Object.keys(user).length).toEqual(6);
        done();
      });
    });
  });

  it("retrieve user data from fetch, make sure the session token hasn't changed", done => {
    const user = new Parse.User();
    user.setPassword('asdf');
    user.setUsername('zxcv');
    let currentSessionToken = '';
    Promise.resolve()
      .then(function () {
        return user.signUp();
      })
      .then(function () {
        currentSessionToken = user.getSessionToken();
        return user.fetch();
      })
      .then(
        function (u) {
          expect(currentSessionToken).toEqual(u.getSessionToken());
          done();
        },
        function (error) {
          ok(false, error);
          done();
        }
      );
  });

  it('user save should fail with invalid email', done => {
    const user = new Parse.User();
    user.set('username', 'teste');
    user.set('password', 'test');
    user.set('email', 'invalid');
    user.signUp().then(
      () => {
        fail('Should not have been able to save.');
        done();
      },
      error => {
        expect(error.code).toEqual(125);
        done();
      }
    );
  });

  it('user signup should error if email taken', done => {
    const user = new Parse.User();
    user.set('username', 'test1');
    user.set('password', 'test');
    user.set('email', 'test@test.com');
    user
      .signUp()
      .then(() => {
        const user2 = new Parse.User();
        user2.set('username', 'test2');
        user2.set('password', 'test');
        user2.set('email', 'test@test.com');
        return user2.signUp();
      })
      .then(
        () => {
          fail('Should not have been able to sign up.');
          done();
        },
        () => {
          done();
        }
      );
  });

  describe('case insensitive signup not allowed', () => {
    it_id('464eddc2-7a46-413d-888e-b43b040f1511')(it)('signup should fail with duplicate case insensitive username with basic setter', async () => {
      const user = new Parse.User();
      user.set('username', 'test1');
      user.set('password', 'test');
      await user.signUp();

      const user2 = new Parse.User();
      user2.set('username', 'Test1');
      user2.set('password', 'test');
      await expectAsync(user2.signUp()).toBeRejectedWith(
        new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.')
      );
    });

    it_id('1cef005b-d5f0-4699-af0c-bb0af27d2437')(it)('signup should fail with duplicate case insensitive username with field specific setter', async () => {
      const user = new Parse.User();
      user.setUsername('test1');
      user.setPassword('test');
      await user.signUp();

      const user2 = new Parse.User();
      user2.setUsername('Test1');
      user2.setPassword('test');
      await expectAsync(user2.signUp()).toBeRejectedWith(
        new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.')
      );
    });

    it_id('12735529-98d1-42c0-b437-3b47fe78ddde')(it)('signup should fail with duplicate case insensitive email', async () => {
      const user = new Parse.User();
      user.setUsername('test1');
      user.setPassword('test');
      user.setEmail('test@example.com');
      await user.signUp();

      const user2 = new Parse.User();
      user2.setUsername('test2');
      user2.setPassword('test');
      user2.setEmail('Test@Example.Com');
      await expectAsync(user2.signUp()).toBeRejectedWith(
        new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.')
      );
    });

    it_id('66e51d52-2420-4b62-8a0d-c7e1b384763e')(it)('edit should fail with duplicate case insensitive email', async () => {
      const user = new Parse.User();
      user.setUsername('test1');
      user.setPassword('test');
      user.setEmail('test@example.com');
      await user.signUp();

      const user2 = new Parse.User();
      user2.setUsername('test2');
      user2.setPassword('test');
      user2.setEmail('Foo@Example.Com');
      await user2.signUp();

      user2.setEmail('Test@Example.Com');
      await expectAsync(user2.save()).toBeRejectedWith(
        new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.')
      );
    });

    describe('anonymous users', () => {
      it('should not fail on case insensitive matches', async () => {
        spyOn(cryptoUtils, 'randomString').and.returnValue('abcdefghijklmnop');
        const logIn = id => Parse.User.logInWith('anonymous', { authData: { id } });
        const user1 = await logIn('test1');
        const username1 = user1.get('username');

        cryptoUtils.randomString.and.returnValue('ABCDEFGHIJKLMNOp');
        const user2 = await logIn('test2');
        const username2 = user2.get('username');

        expect(username1).not.toBeUndefined();
        expect(username2).not.toBeUndefined();
        expect(username1.toLowerCase()).toBe('abcdefghijklmnop');
        expect(username2.toLowerCase()).toBe('abcdefghijklmnop');
        expect(username2).not.toBe(username1);
        expect(username2.toLowerCase()).toBe(username1.toLowerCase()); // this is redundant :).
      });
    });
  });

  it('user cannot update email to existing user', done => {
    const user = new Parse.User();
    user.set('username', 'test1');
    user.set('password', 'test');
    user.set('email', 'test@test.com');
    user
      .signUp()
      .then(() => {
        const user2 = new Parse.User();
        user2.set('username', 'test2');
        user2.set('password', 'test');
        return user2.signUp();
      })
      .then(user2 => {
        user2.set('email', 'test@test.com');
        return user2.save();
      })
      .then(
        () => {
          fail('Should not have been able to sign up.');
          done();
        },
        () => {
          done();
        }
      );
  });

  it('unset user email', done => {
    const user = new Parse.User();
    user.set('username', 'test');
    user.set('password', 'test');
    user.set('email', 'test@test.com');
    user
      .signUp()
      .then(() => {
        user.unset('email');
        return user.save();
      })
      .then(() => {
        return Parse.User.logIn('test', 'test');
      })
      .then(user => {
        expect(user.getEmail()).toBeUndefined();
        done();
      });
  });

  it('create session from user', done => {
    Promise.resolve()
      .then(() => {
        return Parse.User.signUp('finn', 'human', { foo: 'bar' });
      })
      .then(user => {
        request({
          method: 'POST',
          headers: {
            'X-Parse-Application-Id': 'test',
            'X-Parse-Session-Token': user.getSessionToken(),
            'X-Parse-REST-API-Key': 'rest',
          },
          url: 'http://localhost:8378/1/sessions',
        }).then(response => {
          const b = response.data;
          expect(typeof b.sessionToken).toEqual('string');
          expect(typeof b.createdWith).toEqual('object');
          expect(b.createdWith.action).toEqual('create');
          expect(typeof b.user).toEqual('object');
          expect(b.user.objectId).toEqual(user.id);
          done();
        });
      });
  });

  it('user get session from token on signup', async () => {
    const user = await Parse.User.signUp('finn', 'human', { foo: 'bar' });
    const response = await request({
      headers: {
        'X-Parse-Application-Id': 'test',
        'X-Parse-Session-Token': user.getSessionToken(),
        'X-Parse-REST-API-Key': 'rest',
      },
      url: 'http://localhost:8378/1/sessions/me',
    });
    const data = response.data;
    expect(typeof data.sessionToken).toEqual('string');
    expect(typeof data.createdWith).toEqual('object');
    expect(data.createdWith.action).toEqual('signup');
    expect(data.createdWith.authProvider).toEqual('password');
    expect(typeof data.user).toEqual('object');
    expect(data.user.objectId).toEqual(user.id);
  });

  it('user get session from token on username/password login', async () => {
    await Parse.User.signUp('finn', 'human', { foo: 'bar' });
    await Parse.User.logOut();
    const user = await Parse.User.logIn('finn', 'human');
    const response = await request({
      headers: {
        'X-Parse-Application-Id': 'test',
        'X-Parse-Session-Token': user.getSessionToken(),
        'X-Parse-REST-API-Key': 'rest',
      },
      url: 'http://localhost:8378/1/sessions/me',
    });
    const data = response.data;
    expect(typeof data.sessionToken).toEqual('string');
    expect(typeof data.createdWith).toEqual('object');
    expect(data.createdWith.action).toEqual('login');
    expect(data.createdWith.authProvider).toEqual('password');
    expect(typeof data.user).toEqual('object');
    expect(data.user.objectId).toEqual(user.id);
  });

  it('user get session from token on anonymous login', async () => {
    const user = await Parse.AnonymousUtils.logIn();
    const response = await request({
      headers: {
        'X-Parse-Application-Id': 'test',
        'X-Parse-Session-Token': user.getSessionToken(),
        'X-Parse-REST-API-Key': 'rest',
      },
      url: 'http://localhost:8378/1/sessions/me',
    });
    const data = response.data;
    expect(typeof data.sessionToken).toEqual('string');
    expect(typeof data.createdWith).toEqual('object');
    expect(data.createdWith.action).toEqual('login');
    expect(data.createdWith.authProvider).toEqual('anonymous');
    expect(typeof data.user).toEqual('object');
    expect(data.user.objectId).toEqual(user.id);
  });

  it('user update session with other field', done => {
    Promise.resolve()
      .then(() => {
        return Parse.User.signUp('finn', 'human', { foo: 'bar' });
      })
      .then(user => {
        request({
          headers: {
            'X-Parse-Application-Id': 'test',
            'X-Parse-Session-Token': user.getSessionToken(),
            'X-Parse-REST-API-Key': 'rest',
          },
          url: 'http://localhost:8378/1/sessions/me',
        }).then(response => {
          const b = response.data;
          request({
            method: 'PUT',
            headers: {
              'X-Parse-Application-Id': 'test',
              'X-Parse-Session-Token': user.getSessionToken(),
              'X-Parse-REST-API-Key': 'rest',
            },
            url: 'http://localhost:8378/1/sessions/' + b.objectId,
            body: JSON.stringify({ foo: 'bar' }),
          }).then(() => {
            done();
          });
        });
      });
  });

  it('cannot update session if invalid or no session token', done => {
    Promise.resolve()
      .then(() => {
        return Parse.User.signUp('finn', 'human', { foo: 'bar' });
      })
      .then(user => {
        request({
          headers: {
            'X-Parse-Application-Id': 'test',
            'X-Parse-Session-Token': user.getSessionToken(),
            'X-Parse-REST-API-Key': 'rest',
          },
          url: 'http://localhost:8378/1/sessions/me',
        }).then(response => {
          const b = response.data;
          request({
            method: 'PUT',
            headers: {
              'X-Parse-Application-Id': 'test',
              'X-Parse-Session-Token': 'foo',
              'X-Parse-REST-API-Key': 'rest',
              'Content-Type': 'application/json',
            },
            url: 'http://localhost:8378/1/sessions/' + b.objectId,
            body: JSON.stringify({ foo: 'bar' }),
          }).then(fail, response => {
            const b = response.data;
            expect(b.error).toBe('Invalid session token');
            request({
              method: 'PUT',
              headers: {
                'X-Parse-Application-Id': 'test',
                'X-Parse-REST-API-Key': 'rest',
              },
              url: 'http://localhost:8378/1/sessions/' + b.objectId,
              body: JSON.stringify({ foo: 'bar' }),
            }).then(fail, response => {
              const b = response.data;
              expect(b.error).toBe('Session token required.');
              done();
            });
          });
        });
      });
  });

  it('get session only for current user', done => {
    Promise.resolve()
      .then(() => {
        return Parse.User.signUp('test1', 'test', { foo: 'bar' });
      })
      .then(() => {
        return Parse.User.signUp('test2', 'test', { foo: 'bar' });
      })
      .then(user => {
        request({
          headers: {
            'X-Parse-Application-Id': 'test',
            'X-Parse-Session-Token': user.getSessionToken(),
            'X-Parse-REST-API-Key': 'rest',
          },
          url: 'http://localhost:8378/1/sessions',
        }).then(response => {
          const b = response.data;
          expect(b.results.length).toEqual(1);
          expect(typeof b.results[0].user).toEqual('object');
          expect(b.results[0].user.objectId).toEqual(user.id);
          done();
        });
      });
  });

  it('delete session by object', done => {
    Promise.resolve()
      .then(() => {
        return Parse.User.signUp('test1', 'test', { foo: 'bar' });
      })
      .then(() => {
        return Parse.User.signUp('test2', 'test', { foo: 'bar' });
      })
      .then(user => {
        request({
          headers: {
            'X-Parse-Application-Id': 'test',
            'X-Parse-Session-Token': user.getSessionToken(),
            'X-Parse-REST-API-Key': 'rest',
          },
          url: 'http://localhost:8378/1/sessions',
        }).then(response => {
          const b = response.data;
          let objId;
          try {
            expect(b.results.length).toEqual(1);
            objId = b.results[0].objectId;
          } catch (e) {
            jfail(e);
            done();
            return;
          }
          request({
            method: 'DELETE',
            headers: {
              'X-Parse-Application-Id': 'test',
              'X-Parse-Session-Token': user.getSessionToken(),
              'X-Parse-REST-API-Key': 'rest',
            },
            url: 'http://localhost:8378/1/sessions/' + objId,
          }).then(() => {
            request({
              headers: {
                'X-Parse-Application-Id': 'test',
                'X-Parse-Session-Token': user.getSessionToken(),
                'X-Parse-REST-API-Key': 'rest',
              },
              url: 'http://localhost:8378/1/sessions',
            }).then(fail, response => {
              const b = response.data;
              expect(b.code).toEqual(209);
              expect(b.error).toBe('Invalid session token');
              done();
            });
          });
        });
      });
  });

  it('cannot delete session if no sessionToken', done => {
    Promise.resolve()
      .then(() => {
        return Parse.User.signUp('test1', 'test', { foo: 'bar' });
      })
      .then(() => {
        return Parse.User.signUp('test2', 'test', { foo: 'bar' });
      })
      .then(user => {
        request({
          headers: {
            'X-Parse-Application-Id': 'test',
            'X-Parse-Session-Token': user.getSessionToken(),
            'X-Parse-REST-API-Key': 'rest',
          },
          url: 'http://localhost:8378/1/sessions',
        }).then(response => {
          const b = response.data;
          expect(b.results.length).toEqual(1);
          const objId = b.results[0].objectId;
          request({
            method: 'DELETE',
            headers: {
              'X-Parse-Application-Id': 'test',
              'X-Parse-REST-API-Key': 'rest',
            },
            url: 'http://localhost:8378/1/sessions/' + objId,
          }).then(fail, response => {
            const b = response.data;
            expect(b.code).toEqual(209);
            expect(b.error).toBe('Invalid session token');
            done();
          });
        });
      });
  });

  it('password format matches hosted parse', done => {
    const hashed = '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie';
    passwordCrypto.compare('test', hashed).then(
      pass => {
        expect(pass).toBe(true);
        done();
      },
      () => {
        fail('Password format did not match.');
        done();
      }
    );
  });

  it('changing password clears sessions', done => {
    let sessionToken = null;

    Promise.resolve()
      .then(function () {
        return Parse.User.signUp('fosco', 'parse');
      })
      .then(function (newUser) {
        equal(Parse.User.current(), newUser);
        sessionToken = newUser.getSessionToken();
        ok(sessionToken);
        newUser.set('password', 'facebook');
        return newUser.save();
      })
      .then(function () {
        return Parse.User.become(sessionToken);
      })
      .then(
        function () {
          fail('Session should have been invalidated');
          done();
        },
        function (err) {
          expect(err.code).toBe(Parse.Error.INVALID_SESSION_TOKEN);
          expect(err.message).toBe('Invalid session token');
          done();
        }
      );
  });

  it('test parse user become', done => {
    let sessionToken = null;
    Promise.resolve()
      .then(function () {
        return Parse.User.signUp('flessard', 'folo', { foo: 1 });
      })
      .then(function (newUser) {
        equal(Parse.User.current(), newUser);
        sessionToken = newUser.getSessionToken();
        ok(sessionToken);
        newUser.set('foo', 2);
        return newUser.save();
      })
      .then(function () {
        return Parse.User.become(sessionToken);
      })
      .then(
        function (newUser) {
          equal(newUser.get('foo'), 2);
          done();
        },
        function () {
          fail('The session should still be valid');
          done();
        }
      );
  });

  it('ensure logout works', done => {
    let user = null;
    let sessionToken = null;

    Promise.resolve()
      .then(function () {
        return Parse.User.signUp('log', 'out');
      })
      .then(newUser => {
        user = newUser;
        sessionToken = user.getSessionToken();
        return Parse.User.logOut();
      })
      .then(() => {
        user.set('foo', 'bar');
        return user.save(null, { sessionToken: sessionToken });
      })
      .then(
        () => {
          fail('Save should have failed.');
          done();
        },
        e => {
          expect(e.code).toEqual(Parse.Error.INVALID_SESSION_TOKEN);
          done();
        }
      );
  });

  it('support user/password signup with empty authData block', done => {
    // The android SDK can send an empty authData object along with username and password.
    Parse.User.signUp('artof', 'thedeal', { authData: {} }).then(
      () => {
        done();
      },
      () => {
        fail('Signup should have succeeded.');
        done();
      }
    );
  });

  it('session expiresAt correct format', async done => {
    await Parse.User.signUp('asdf', 'zxcv');
    request({
      url: 'http://localhost:8378/1/classes/_Session',
      headers: {
        'X-Parse-Application-Id': 'test',
        'X-Parse-Master-Key': 'test',
      },
    }).then(response => {
      const body = response.data;
      expect(body.results[0].expiresAt.__type).toEqual('Date');
      done();
    });
  });

  it('Invalid session tokens are rejected', async done => {
    await Parse.User.signUp('asdf', 'zxcv');
    request({
      url: 'http://localhost:8378/1/classes/AClass',
      headers: {
        'X-Parse-Application-Id': 'test',
        'X-Parse-Rest-API-Key': 'rest',
        'X-Parse-Session-Token': 'text',
      },
    }).then(fail, response => {
      const body = response.data;
      expect(body.code).toBe(209);
      expect(body.error).toBe('Invalid session token');
      done();
    });
  });

  it_exclude_dbs(['postgres'])(
    'should cleanup null authData keys (regression test for #935)',
    done => {
      const database = Config.get(Parse.applicationId).database;
      database
        .create(
          '_User',
          {
            username: 'user',
            _hashed_password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
            _auth_data_facebook: null,
          },
          {}
        )
        .then(() => {
          return request({
            url: 'http://localhost:8378/1/login?username=user&password=test',
            headers: {
              'X-Parse-Application-Id': 'test',
              'X-Parse-Master-Key': 'test',
            },
          }).then(res => res.data);
        })
        .then(user => {
          const authData = user.authData;
          expect(user.username).toEqual('user');
          expect(authData).toBeUndefined();
          done();
        })
        .catch(() => {
          fail('this should not fail');
          done();
        });
    }
  );

  it_exclude_dbs(['postgres'])('should not serve null authData keys', done => {
    const database = Config.get(Parse.applicationId).database;
    database
      .create(
        '_User',
        {
          username: 'user',
          _hashed_password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
          _auth_data_facebook: null,
        },
        {}
      )
      .then(() => {
        return new Parse.Query(Parse.User)
          .equalTo('username', 'user')
          .first({ useMasterKey: true });
      })
      .then(user => {
        const authData = user.get('authData');
        expect(user.get('username')).toEqual('user');
        expect(authData).toBeUndefined();
        done();
      })
      .catch(() => {
        fail('this should not fail');
        done();
      });
  });

  it('should cleanup null authData keys ParseUser update (regression test for #1198, #2252)', done => {
    Parse.Cloud.beforeSave('_User', req => {
      req.object.set('foo', 'bar');
    });

    let originalSessionToken;
    let originalUserId;
    // Simulate anonymous user save
    request({
      method: 'POST',
      url: 'http://localhost:8378/1/classes/_User',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        authData: {
          anonymous: { id: '00000000-0000-0000-0000-000000000001' },
        },
      },
    })
      .then(response => response.data)
      .then(user => {
        originalSessionToken = user.sessionToken;
        originalUserId = user.objectId;
        // Simulate registration
        return request({
          method: 'PUT',
          url: 'http://localhost:8378/1/classes/_User/' + user.objectId,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-Session-Token': user.sessionToken,
            'X-Parse-REST-API-Key': 'rest',
            'Content-Type': 'application/json',
          },
          body: {
            authData: { anonymous: null },
            username: 'user',
            password: 'password',
          },
        }).then(response => {
          return response.data;
        });
      })
      .then(user => {
        expect(typeof user).toEqual('object');
        expect(user.authData).toBeUndefined();
        expect(user.sessionToken).not.toBeUndefined();
        // Session token should have changed
        expect(user.sessionToken).not.toEqual(originalSessionToken);
        // test that the sessionToken is valid
        return request({
          url: 'http://localhost:8378/1/users/me',
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-Session-Token': user.sessionToken,
            'X-Parse-REST-API-Key': 'rest',
            'Content-Type': 'application/json',
          },
        }).then(response => {
          const body = response.data;
          expect(body.username).toEqual('user');
          expect(body.objectId).toEqual(originalUserId);
          done();
        });
      })
      .catch(err => {
        fail('no request should fail: ' + JSON.stringify(err));
        done();
      });
  });

  it_id('1be98368-19ac-4c77-8531-762a114f43fb')(it)('should send email when upgrading from anon', async done => {
    await reconfigureServer();
    let emailCalled = false;
    let emailOptions;
    const emailAdapter = {
      sendVerificationEmail: options => {
        emailOptions = options;
        emailCalled = true;
      },
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };
    await reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    });
    // Simulate anonymous user save
    return request({
      method: 'POST',
      url: 'http://localhost:8378/1/classes/_User',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        authData: {
          anonymous: { id: '00000000-0000-0000-0000-000000000001' },
        },
      },
    })
      .then(response => {
        const user = response.data;
        return request({
          method: 'PUT',
          url: 'http://localhost:8378/1/classes/_User/' + user.objectId,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-Session-Token': user.sessionToken,
            'X-Parse-REST-API-Key': 'rest',
            'Content-Type': 'application/json',
          },
          body: {
            authData: { anonymous: null },
            username: 'user',
            email: 'user@email.com',
            password: 'password',
          },
        });
      })
      .then(() => jasmine.timeout())
      .then(() => {
        expect(emailCalled).toBe(true);
        expect(emailOptions).not.toBeUndefined();
        expect(emailOptions.user.get('email')).toEqual('user@email.com');
        done();
      })
      .catch(err => {
        jfail(err);
        fail('no request should fail: ' + JSON.stringify(err));
        done();
      });
  });

  it_id('bf668670-39fa-44d3-a9a9-cad52f36d272')(it)('should not send email when email is not a string', async done => {
    let emailCalled = false;
    let emailOptions;
    const emailAdapter = {
      sendVerificationEmail: options => {
        emailOptions = options;
        emailCalled = true;
      },
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };
    await reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    });
    const user = new Parse.User();
    user.set('username', 'asdf@jkl.com');
    user.set('password', 'zxcv');
    user.set('email', 'asdf@jkl.com');
    await user.signUp();
    request({
      method: 'POST',
      url: 'http://localhost:8378/1/requestPasswordReset',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-Session-Token': user.sessionToken,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        email: { $regex: '^asd' },
      },
    })
      .then(res => {
        fail('no request should succeed: ' + JSON.stringify(res));
        done();
      })
      .catch(err => {
        expect(emailCalled).toBeTruthy();
        expect(emailOptions).toBeDefined();
        expect(err.status).toBe(400);
        expect(err.text).toMatch('{"code":125,"error":"you must provide a valid email string"}');
        done();
      });
  });

  it('should aftersave with full object', done => {
    let hit = 0;
    Parse.Cloud.afterSave('_User', (req, res) => {
      hit++;
      expect(req.object.get('username')).toEqual('User');
      res.success();
    });
    const user = new Parse.User();
    user.setUsername('User');
    user.setPassword('pass');
    user
      .signUp()
      .then(() => {
        user.set('hello', 'world');
        return user.save();
      })
      .then(() => {
        expect(hit).toBe(2);
        done();
      });
  });

  it('changes to a user should update the cache', done => {
    Parse.Cloud.define('testUpdatedUser', req => {
      expect(req.user.get('han')).toEqual('solo');
      return {};
    });
    const user = new Parse.User();
    user.setUsername('harrison');
    user.setPassword('ford');
    user
      .signUp()
      .then(() => {
        user.set('han', 'solo');
        return user.save();
      })
      .then(() => {
        return Parse.Cloud.run('testUpdatedUser');
      })
      .then(
        () => {
          done();
        },
        () => {
          fail('Should not have failed.');
          done();
        }
      );
  });

  it('should fail to become user with expired token', done => {
    let token;
    Parse.User.signUp('auser', 'somepass', null)
      .then(() =>
        request({
          method: 'GET',
          url: 'http://localhost:8378/1/classes/_Session',
          headers: {
            'X-Parse-Application-Id': 'test',
            'X-Parse-Master-Key': 'test',
          },
        })
      )
      .then(response => {
        const body = response.data;
        const id = body.results[0].objectId;
        const expiresAt = new Date(new Date().setYear(2015));
        token = body.results[0].sessionToken;
        return request({
          method: 'PUT',
          url: 'http://localhost:8378/1/classes/_Session/' + id,
          headers: {
            'X-Parse-Application-Id': 'test',
            'X-Parse-Master-Key': 'test',
            'Content-Type': 'application/json',
          },
          body: {
            expiresAt: { __type: 'Date', iso: expiresAt.toISOString() },
          },
        });
      })
      .then(() => Parse.User.become(token))
      .then(
        () => {
          fail('Should not have succeded');
          done();
        },
        error => {
          expect(error.code).toEqual(209);
          expect(error.message).toEqual('Session token is expired.');
          done();
        }
      )
      .catch(done.fail);
  });

  it('should return current session with expired expiration date', async () => {
    await Parse.User.signUp('buser', 'somepass', null);
    const response = await request({
      method: 'GET',
      url: 'http://localhost:8378/1/classes/_Session',
      headers: {
        'X-Parse-Application-Id': 'test',
        'X-Parse-Master-Key': 'test',
      },
    });
    const body = response.data;
    const id = body.results[0].objectId;
    const expiresAt = new Date(new Date().setYear(2015));
    await request({
      method: 'PUT',
      url: 'http://localhost:8378/1/classes/_Session/' + id,
      headers: {
        'X-Parse-Application-Id': 'test',
        'X-Parse-Master-Key': 'test',
        'Content-Type': 'application/json',
      },
      body: {
        expiresAt: { __type: 'Date', iso: expiresAt.toISOString() },
      },
    });
    const session = await Parse.Session.current();
    expect(session.get('expiresAt')).toEqual(expiresAt);
  });

  it('should not create extraneous session tokens', done => {
    const config = Config.get(Parse.applicationId);
    config.database
      .loadSchema()
      .then(s => {
        // Lock down the _User class for creation
        return s.addClassIfNotExists('_User', {}, { create: {} });
      })
      .then(() => {
        const user = new Parse.User();
        return user.save({ username: 'user', password: 'pass' });
      })
      .then(
        () => {
          fail('should not be able to save the user');
        },
        () => {
          return Promise.resolve();
        }
      )
      .then(() => {
        const q = new Parse.Query('_Session');
        return q.find({ useMasterKey: true });
      })
      .then(
        res => {
          // We should have no session created
          expect(res.length).toBe(0);
          done();
        },
        () => {
          fail('should not fail');
          done();
        }
      );
  });

  it('should not overwrite username when unlinking facebook user (regression test for #1532)', async done => {
    Parse.Object.disableSingleInstance();
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);
    let user = new Parse.User();
    user.set('username', 'testLinkWithProvider');
    user.set('password', 'mypass');
    await user.signUp();
    await user._linkWith('facebook');
    expect(user.get('username')).toEqual('testLinkWithProvider');
    expect(Parse.FacebookUtils.isLinked(user)).toBeTruthy();
    await user._unlinkFrom('facebook');
    user = await user.fetch();
    expect(user.get('username')).toEqual('testLinkWithProvider');
    expect(Parse.FacebookUtils.isLinked(user)).toBeFalsy();
    done();
  });

  it('should revoke sessions when converting anonymous user to "normal" user', done => {
    request({
      method: 'POST',
      url: 'http://localhost:8378/1/classes/_User',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        authData: {
          anonymous: { id: '00000000-0000-0000-0000-000000000001' },
        },
      },
    }).then(response => {
      const body = response.data;
      Parse.User.become(body.sessionToken).then(user => {
        const obj = new Parse.Object('TestObject');
        obj.setACL(new Parse.ACL(user));
        return obj
          .save()
          .then(() => {
            // Change password, revoking session
            user.set('username', 'no longer anonymous');
            user.set('password', 'password');
            return user.save();
          })
          .then(() => {
            // Session token should have been recycled
            expect(body.sessionToken).not.toEqual(user.getSessionToken());
          })
          .then(() => obj.fetch())
          .then(() => {
            done();
          })
          .catch(() => {
            fail('should not fail');
            done();
          });
      });
    });
  });

  it('should not revoke session tokens if the server is configures to not revoke session tokens', done => {
    reconfigureServer({ revokeSessionOnPasswordReset: false }).then(() => {
      request({
        method: 'POST',
        url: 'http://localhost:8378/1/classes/_User',
        headers: {
          'X-Parse-Application-Id': Parse.applicationId,
          'X-Parse-REST-API-Key': 'rest',
          'Content-Type': 'application/json',
        },
        body: {
          authData: {
            anonymous: { id: '00000000-0000-0000-0000-000000000001' },
          },
        },
      }).then(response => {
        const body = response.data;
        Parse.User.become(body.sessionToken).then(user => {
          const obj = new Parse.Object('TestObject');
          obj.setACL(new Parse.ACL(user));
          return (
            obj
              .save()
              .then(() => {
                // Change password, revoking session
                user.set('username', 'no longer anonymous');
                user.set('password', 'password');
                return user.save();
              })
              .then(() => obj.fetch())
              // fetch should succeed as we still have our session token
              .then(done, fail)
          );
        });
      });
    });
  });

  it('should not fail querying non existing relations', done => {
    const user = new Parse.User();
    user.set({
      username: 'hello',
      password: 'world',
    });
    user
      .signUp()
      .then(() => {
        return Parse.User.current().relation('relation').query().find();
      })
      .then(res => {
        expect(res.length).toBe(0);
        done();
      })
      .catch(err => {
        fail(JSON.stringify(err));
        done();
      });
  });

  it('should not allow updates to emailVerified', done => {
    const emailAdapter = {
      sendVerificationEmail: () => {},
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };

    const user = new Parse.User();
    user.set({
      username: 'hello',
      password: 'world',
      email: 'test@email.com',
    });

    reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        return user.signUp();
      })
      .then(() => {
        return Parse.User.current().set('emailVerified', true).save();
      })
      .then(() => {
        fail('Should not be able to update emailVerified');
        done();
      })
      .catch(err => {
        expect(err.message).toBe("Clients aren't allowed to manually update email verification.");
        done();
      });
  });

  it('should not retrieve hidden fields on GET users/me (#3432)', done => {
    const emailAdapter = {
      sendVerificationEmail: () => {},
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };

    const user = new Parse.User();
    user.set({
      username: 'hello',
      password: 'world',
      email: 'test@email.com',
    });

    reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        return user.signUp();
      })
      .then(() =>
        request({
          method: 'GET',
          url: 'http://localhost:8378/1/users/me',
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-Session-Token': Parse.User.current().getSessionToken(),
            'X-Parse-REST-API-Key': 'rest',
          },
        })
      )
      .then(response => {
        const res = response.data;
        expect(res.emailVerified).toBe(false);
        expect(res._email_verify_token).toBeUndefined();
        done();
      })
      .catch(done.fail);
  });

  it('should not retrieve hidden fields on GET users/id (#3432)', done => {
    const emailAdapter = {
      sendVerificationEmail: () => {},
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };

    const user = new Parse.User();
    user.set({
      username: 'hello',
      password: 'world',
      email: 'test@email.com',
    });
    const acl = new Parse.ACL();
    acl.setPublicReadAccess(true);
    user.setACL(acl);

    reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        return user.signUp();
      })
      .then(() =>
        request({
          method: 'GET',
          url: 'http://localhost:8378/1/users/' + Parse.User.current().id,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
        })
      )
      .then(response => {
        const res = response.data;
        expect(res.emailVerified).toBe(false);
        expect(res._email_verify_token).toBeUndefined();
        done();
      })
      .catch(err => {
        fail(JSON.stringify(err));
        done();
      });
  });

  it('should not retrieve hidden fields on login (#3432)', done => {
    const emailAdapter = {
      sendVerificationEmail: () => {},
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };

    const user = new Parse.User();
    user.set({
      username: 'hello',
      password: 'world',
      email: 'test@email.com',
    });

    reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        return user.signUp();
      })
      .then(() =>
        request({
          url: 'http://localhost:8378/1/login?email=test@email.com&username=hello&password=world',
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
        })
      )
      .then(response => {
        const res = response.data;
        expect(res.emailVerified).toBe(false);
        expect(res._email_verify_token).toBeUndefined();
        done();
      })
      .catch(err => {
        fail(JSON.stringify(err));
        done();
      });
  });

  it('should not allow updates to hidden fields', async () => {
    const emailAdapter = {
      sendVerificationEmail: () => {},
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };
    const user = new Parse.User();
    user.set({
      username: 'hello',
      password: 'world',
      email: 'test@email.com',
    });
    await reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    });
    await user.signUp();
    user.set('_email_verify_token', 'bad', { ignoreValidation: true });
    await expectAsync(user.save()).toBeRejectedWith(
      new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: _email_verify_token.')
    );
  });

  it('should allow updates to fields with maintenanceKey', async () => {
    const emailAdapter = {
      sendVerificationEmail: () => {},
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };
    const user = new Parse.User();
    user.set({
      username: 'hello',
      password: 'world',
      email: 'test@example.com',
    });
    await reconfigureServer({
      appName: 'unused',
      maintenanceKey: 'test2',
      verifyUserEmails: true,
      emailVerifyTokenValidityDuration: 5,
      accountLockout: {
        duration: 1,
        threshold: 1,
      },
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    });
    await user.signUp();
    for (let i = 0; i < 2; i++) {
      try {
        await Parse.User.logIn(user.getEmail(), 'abc');
      } catch (e) {
        expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
        expect(
          e.message === 'Invalid username/password.' ||
            e.message ===
              'Your account is locked due to multiple failed login attempts. Please try again after 1 minute(s)'
        ).toBeTrue();
      }
    }
    await Parse.User.requestPasswordReset(user.getEmail());
    const headers = {
      'X-Parse-Application-Id': 'test',
      'X-Parse-Rest-API-Key': 'rest',
      'X-Parse-Maintenance-Key': 'test2',
      'Content-Type': 'application/json',
    };
    const userMaster = await request({
      method: 'GET',
      url: `http://localhost:8378/1/classes/_User`,
      json: true,
      headers,
    }).then(res => res.data.results[0]);
    expect(Object.keys(userMaster).sort()).toEqual(
      [
        'ACL',
        '_account_lockout_expires_at',
        '_email_verify_token',
        '_email_verify_token_expires_at',
        '_failed_login_count',
        '_perishable_token',
        'createdAt',
        'email',
        'emailVerified',
        'objectId',
        'updatedAt',
        'username',
      ].sort()
    );
    const toSet = {
      _account_lockout_expires_at: new Date(),
      _email_verify_token: 'abc',
      _email_verify_token_expires_at: new Date(),
      _failed_login_count: 0,
      _perishable_token_expires_at: new Date(),
      _perishable_token: 'abc',
    };
    await request({
      method: 'PUT',
      headers,
      url: Parse.serverURL + '/users/' + userMaster.objectId,
      json: true,
      body: toSet,
    }).then(res => res.data);
    const update = await request({
      method: 'GET',
      url: `http://localhost:8378/1/classes/_User`,
      json: true,
      headers,
    }).then(res => res.data.results[0]);
    for (const key in toSet) {
      const value = toSet[key];
      if (update[key] && update[key].iso) {
        expect(update[key].iso).toEqual(value.toISOString());
      } else if (value.toISOString) {
        expect(update[key]).toEqual(value.toISOString());
      } else {
        expect(update[key]).toEqual(value);
      }
    }
  });

  it('should revoke sessions when setting paswword with masterKey (#3289)', done => {
    let user;
    Parse.User.signUp('username', 'password')
      .then(newUser => {
        user = newUser;
        user.set('password', 'newPassword');
        return user.save(null, { useMasterKey: true });
      })
      .then(() => {
        const query = new Parse.Query('_Session');
        query.equalTo('user', user);
        return query.find({ useMasterKey: true });
      })
      .then(results => {
        expect(results.length).toBe(0);
        done();
      }, done.fail);
  });

  xit('should not send a verification email if the user signed up using oauth', done => {
    let emailCalledCount = 0;
    const emailAdapter = {
      sendVerificationEmail: () => {
        emailCalledCount++;
        return Promise.resolve();
      },
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => Promise.resolve(),
    };
    reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      emailAdapter: emailAdapter,
      publicServerURL: 'http://localhost:8378/1',
    });
    const user = new Parse.User();
    user.set('email', 'email1@host.com');
    Parse.FacebookUtils.link(user, {
      id: '8675309',
      access_token: 'jenny',
      expiration_date: new Date().toJSON(),
    }).then(user => {
      user.set('email', 'email2@host.com');
      user.save().then(() => {
        expect(emailCalledCount).toBe(0);
        done();
      });
    });
  }).pend('this test fails.  See: https://github.com/parse-community/parse-server/issues/5097');

  it('should be able to update user with authData passed', done => {
    let objectId;
    let sessionToken;

    function validate(block) {
      return request({
        url: `http://localhost:8378/1/classes/_User/${objectId}`,
        headers: {
          'X-Parse-Application-Id': Parse.applicationId,
          'X-Parse-REST-API-Key': 'rest',
          'X-Parse-Session-Token': sessionToken,
        },
      }).then(response => block(response.data));
    }

    request({
      method: 'POST',
      url: 'http://localhost:8378/1/classes/_User',
      headers: {
        'X-Parse-Application-Id': Parse.applicationId,
        'X-Parse-REST-API-Key': 'rest',
        'Content-Type': 'application/json',
      },
      body: {
        key: 'value',
        authData: { anonymous: { id: '00000000-0000-0000-0000-000000000001' } },
      },
    })
      .then(response => {
        const body = response.data;
        objectId = body.objectId;
        sessionToken = body.sessionToken;
        expect(sessionToken).toBeDefined();
        expect(objectId).toBeDefined();
        return validate(user => {
          // validate that keys are set on creation
          expect(user.key).toBe('value');
        });
      })
      .then(() => {
        // update the user
        const options = {
          method: 'PUT',
          url: `http://localhost:8378/1/classes/_User/${objectId}`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
            'X-Parse-Session-Token': sessionToken,
            'Content-Type': 'application/json',
          },
          body: {
            key: 'otherValue',
            authData: {
              anonymous: { id: '00000000-0000-0000-0000-000000000001' },
            },
          },
        };
        return request(options);
      })
      .then(() => {
        return validate(user => {
          // validate that keys are set on update
          expect(user.key).toBe('otherValue');
        });
      })
      .then(() => {
        done();
      })
      .then(done)
      .catch(done.fail);
  });

  it('can login with email', done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        const options = {
          url: `http://localhost:8378/1/login`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
          qs: { email: 'yo@lo.com', password: 'yolopass' },
        };
        return request(options);
      })
      .then(done)
      .catch(done.fail);
  });

  it('cannot login with email and invalid password', done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        const options = {
          method: 'POST',
          url: `http://localhost:8378/1/login`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
            'Content-Type': 'application/json',
          },
          body: { email: 'yo@lo.com', password: 'yolopass2' },
        };
        return request(options);
      })
      .then(done.fail)
      .catch(() => done());
  });

  it('can login with email through query string', done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        const options = {
          url: `http://localhost:8378/1/login?email=yo@lo.com&password=yolopass`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
        };
        return request(options);
      })
      .then(done)
      .catch(done.fail);
  });

  it('can login when both email and username are passed', done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        const options = {
          url: `http://localhost:8378/1/login?email=yo@lo.com&username=yolo&password=yolopass`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
        };
        return request(options);
      })
      .then(done)
      .catch(done.fail);
  });

  it("fails to login when username doesn't match email", done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        const options = {
          url: `http://localhost:8378/1/login?email=yo@lo.com&username=yolo2&password=yolopass`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
        };
        return request(options);
      })
      .then(done.fail)
      .catch(err => {
        expect(err.data.error).toEqual('Invalid username/password.');
        done();
      });
  });

  it("fails to login when email doesn't match username", done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        const options = {
          url: `http://localhost:8378/1/login?email=yo@lo2.com&username=yolo&password=yolopass`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
        };
        return request(options);
      })
      .then(done.fail)
      .catch(err => {
        expect(err.data.error).toEqual('Invalid username/password.');
        done();
      });
  });

  it('fails to login when email and username are not provided', done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        const options = {
          url: `http://localhost:8378/1/login?password=yolopass`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
        };
        return request(options);
      })
      .then(done.fail)
      .catch(err => {
        expect(err.data.error).toEqual('username/email is required.');
        done();
      });
  });

  it('allows login when providing email as username', done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        return Parse.User.logIn('yo@lo.com', 'yolopass');
      })
      .then(user => {
        expect(user.get('username')).toBe('yolo');
      })
      .then(done)
      .catch(done.fail);
  });

  it('handles properly when 2 users share username / email pairs', done => {
    const user = new Parse.User({
      username: 'yo@loname.com',
      password: 'yolopass',
      email: 'yo@lo.com',
    });
    const user2 = new Parse.User({
      username: 'yo@lo.com',
      email: 'yo@loname.com',
      password: 'yolopass2', // different passwords
    });

    Parse.Object.saveAll([user, user2])
      .then(() => {
        return Parse.User.logIn('yo@loname.com', 'yolopass');
      })
      .then(user => {
        // the username takes precedence over the email,
        // so we get the user with username as passed in
        expect(user.get('username')).toBe('yo@loname.com');
      })
      .then(done)
      .catch(done.fail);
  });

  it('handles properly when 2 users share username / email pairs, counterpart', done => {
    const user = new Parse.User({
      username: 'yo@loname.com',
      password: 'yolopass',
      email: 'yo@lo.com',
    });
    const user2 = new Parse.User({
      username: 'yo@lo.com',
      email: 'yo@loname.com',
      password: 'yolopass2', // different passwords
    });

    Parse.Object.saveAll([user, user2])
      .then(() => {
        return Parse.User.logIn('yo@loname.com', 'yolopass2');
      })
      .then(done.fail)
      .catch(err => {
        expect(err.message).toEqual('Invalid username/password.');
        done();
      });
  });

  it('fails to login when password is not provided', done => {
    const user = new Parse.User();
    user
      .save({
        username: 'yolo',
        password: 'yolopass',
        email: 'yo@lo.com',
      })
      .then(() => {
        const options = {
          url: `http://localhost:8378/1/login?username=yolo`,
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
          },
        };
        return request(options);
      })
      .then(done.fail)
      .catch(err => {
        expect(err.data.error).toEqual('password is required.');
        done();
      });
  });

  it('does not duplicate session when logging in multiple times #3451', done => {
    const user = new Parse.User();
    user
      .signUp({
        username: 'yolo',
        password: 'yolo',
        email: 'yo@lo.com',
      })
      .then(() => {
        const token = user.getSessionToken();
        let promise = Promise.resolve();
        let count = 0;
        while (count < 5) {
          promise = promise.then(() => {
            return Parse.User.logIn('yolo', 'yolo').then(res => {
              // ensure a new session token is generated at each login
              expect(res.getSessionToken()).not.toBe(token);
            });
          });
          count++;
        }
        return promise;
      })
      .then(() => {
        // wait because session destruction is not synchronous
        return new Promise(resolve => {
          setTimeout(resolve, 100);
        });
      })
      .then(() => {
        const query = new Parse.Query('_Session');
        return query.find({ useMasterKey: true });
      })
      .then(results => {
        // only one session in the end
        expect(results.length).toBe(1);
      })
      .then(done, done.fail);
  });

  it('should throw OBJECT_NOT_FOUND instead of SESSION_MISSING when using masterKey', async () => {
    await reconfigureServer();
    // create a fake user (just so we simulate an object not found)
    const non_existent_user = Parse.User.createWithoutData('fake_id');
    try {
      await non_existent_user.destroy({ useMasterKey: true });
      throw '';
    } catch (e) {
      expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
    }
    try {
      await non_existent_user.save({}, { useMasterKey: true });
      throw '';
    } catch (e) {
      expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
    }
    try {
      await non_existent_user.save();
      throw '';
    } catch (e) {
      expect(e.code).toBe(Parse.Error.SESSION_MISSING);
    }
    try {
      await non_existent_user.destroy();
      throw '';
    } catch (e) {
      expect(e.code).toBe(Parse.Error.SESSION_MISSING);
    }
  });

  it('should throw when enforcePrivateUsers is invalid', async () => {
    const options = [[], 'a', 0, {}];
    for (const option of options) {
      await expectAsync(reconfigureServer({ enforcePrivateUsers: option })).toBeRejected();
    }
  });

  it('user login with enforcePrivateUsers', async done => {
    await reconfigureServer({ enforcePrivateUsers: true });
    await Parse.User.signUp('asdf', 'zxcv');
    const user = await Parse.User.logIn('asdf', 'zxcv');
    equal(user.get('username'), 'asdf');
    const ACL = user.getACL();
    expect(ACL.getReadAccess(user)).toBe(true);
    expect(ACL.getWriteAccess(user)).toBe(true);
    expect(ACL.getPublicReadAccess()).toBe(false);
    expect(ACL.getPublicWriteAccess()).toBe(false);
    const perms = ACL.permissionsById;
    expect(Object.keys(perms).length).toBe(1);
    expect(perms[user.id].read).toBe(true);
    expect(perms[user.id].write).toBe(true);
    expect(perms['*']).toBeUndefined();
    done();
  });

  describe('issue #4897', () => {
    it_only_db('mongo')('should be able to login with a legacy user (no ACL)', async () => {
      // This issue is a side effect of the locked users and legacy users which don't have ACL's
      // In this scenario, a legacy user wasn't be able to login as there's no ACL on it
      await reconfigureServer();
      const database = Config.get(Parse.applicationId).database;
      const collection = await database.adapter._adaptiveCollection('_User');
      await collection.insertOne({
        _id: 'ABCDEF1234',
        name: '<some_name>',
        email: '<some_email>',
        username: '<some_username>',
        _hashed_password: '<some_password>',
        _auth_data_facebook: {
          id: '8675309',
          access_token: 'jenny',
        },
        sessionToken: '<some_session_token>',
      });
      const provider = getMockFacebookProvider();
      Parse.User._registerAuthenticationProvider(provider);
      const model = await Parse.User._logInWith('facebook', {});
      expect(model.id).toBe('ABCDEF1234');
      ok(model instanceof Parse.User, 'Model should be a Parse.User');
      strictEqual(Parse.User.current(), model);
      ok(model.extended(), 'Should have used subclass.');
      strictEqual(provider.authData.id, provider.synchronizedUserId);
      strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
      strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
      ok(model._isLinked('facebook'), 'User should be linked to facebook');
    });
  });

  it('should strip out authdata in LiveQuery', async () => {
    const provider = getMockFacebookProvider();
    Parse.User._registerAuthenticationProvider(provider);

    await reconfigureServer({
      liveQuery: { classNames: ['_User'] },
      startLiveQueryServer: true,
      verbose: false,
      silent: true,
    });

    Parse.Cloud.beforeSave(Parse.User, ({ object }) => {
      const acl = new Parse.ACL();
      acl.setPublicReadAccess(true);
      object.setACL(acl);
    });

    const query = new Parse.Query(Parse.User);
    query.doesNotExist('foo');
    const subscription = await query.subscribe();

    const events = ['create', 'update', 'enter', 'leave', 'delete'];
    const response = (obj, prev) => {
      expect(obj.get('authData')).toBeUndefined();
      expect(obj.authData).toBeUndefined();
      expect(prev && prev.authData).toBeUndefined();
      if (prev && prev.get) {
        expect(prev.get('authData')).toBeUndefined();
      }
    };
    const calls = {};
    for (const key of events) {
      calls[key] = response;
      spyOn(calls, key).and.callThrough();
      subscription.on(key, calls[key]);
    }
    const user = await Parse.User._logInWith('facebook');
    user.set('foo', 'bar');
    await user.save();
    user.unset('foo');
    await user.save();
    user.set('yolo', 'bar');
    await user.save();
    await user.destroy();
    await new Promise(resolve => setTimeout(resolve, 10));
    for (const key of events) {
      expect(calls[key]).toHaveBeenCalled();
    }
    subscription.unsubscribe();
    const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
    client.close();
    await new Promise(resolve => setTimeout(resolve, 10));
  });
});

describe('Security Advisory GHSA-8w3j-g983-8jh5', function () {
  it_only_db('mongo')(
    'should validate credentials first and check if account already linked afterwards ()',
    async done => {
      await reconfigureServer();
      // Add User to Database with authData
      const database = Config.get(Parse.applicationId).database;
      const collection = await database.adapter._adaptiveCollection('_User');
      await collection.insertOne({
        _id: 'ABCDEF1234',
        name: '<some_name>',
        email: '<some_email>',
        username: '<some_username>',
        _hashed_password: '<some_password>',
        _auth_data_custom: {
          id: 'linkedID', // Already linked userid
        },
        sessionToken: '<some_session_token>',
      });
      const provider = {
        getAuthType: () => 'custom',
        restoreAuthentication: () => true,
      }; // AuthProvider checks if password is 'password'
      Parse.User._registerAuthenticationProvider(provider);

      // Try to link second user with wrong password
      try {
        const user = await Parse.AnonymousUtils.logIn();
        await user._linkWith(provider.getAuthType(), {
          authData: { id: 'linkedID', password: 'wrong' },
        });
      } catch (error) {
        // This should throw Parse.Error.SESSION_MISSING and not Parse.Error.ACCOUNT_ALREADY_LINKED
        expect(error.code).toEqual(Parse.Error.SESSION_MISSING);
        done();
        return;
      }
      fail();
      done();
    }
  );
  it_only_db('mongo')('should ignore authData field', async () => {
    // Add User to Database with authData
    await reconfigureServer();
    const database = Config.get(Parse.applicationId).database;
    const collection = await database.adapter._adaptiveCollection('_User');
    await collection.insertOne({
      _id: '1234ABCDEF',
      name: '<some_name>',
      email: '<some_email>',
      username: '<some_username>',
      _hashed_password: '<some_password>',
      _auth_data_custom: {
        id: 'linkedID',
      },
      sessionToken: '<some_session_token>',
      authData: null, // should ignore
    });
    const provider = {
      getAuthType: () => 'custom',
      restoreAuthentication: () => true,
    };
    Parse.User._registerAuthenticationProvider(provider);
    const query = new Parse.Query(Parse.User);
    const user = await query.get('1234ABCDEF', { useMasterKey: true });
    expect(user.get('authData')).toEqual({ custom: { id: 'linkedID' } });
  });
});

describe('login as other user', () => {
  it('allows creating a session for another user with the master key', async done => {
    await Parse.User.signUp('some_user', 'some_password');
    const userId = Parse.User.current().id;
    await Parse.User.logOut();

    try {
      const response = await request({
        method: 'POST',
        url: 'http://localhost:8378/1/loginAs',
        headers: {
          'X-Parse-Application-Id': Parse.applicationId,
          'X-Parse-REST-API-Key': 'rest',
          'X-Parse-Master-Key': 'test',
        },
        body: {
          userId,
        },
      });

      expect(response.data.sessionToken).toBeDefined();
    } catch (err) {
      fail(`no request should fail: ${JSON.stringify(err)}`);
      done();
    }

    const sessionsQuery = new Parse.Query(Parse.Session);
    const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
    expect(sessionsAfterRequest.length).toBe(1);

    done();
  });

  it('rejects creating a session for another user if the user does not exist', async done => {
    try {
      await request({
        method: 'POST',
        url: 'http://localhost:8378/1/loginAs',
        headers: {
          'X-Parse-Application-Id': Parse.applicationId,
          'X-Parse-REST-API-Key': 'rest',
          'X-Parse-Master-Key': 'test',
        },
        body: {
          userId: 'bogus-user',
        },
      });

      fail('Request should fail without a valid user ID');
      done();
    } catch (err) {
      expect(err.data.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
      expect(err.data.error).toBe('user not found');
    }

    const sessionsQuery = new Parse.Query(Parse.Session);
    const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
    expect(sessionsAfterRequest.length).toBe(0);

    done();
  });

  it('rejects creating a session for another user with invalid parameters', async done => {
    const invalidUserIds = [undefined, null, ''];

    for (const invalidUserId of invalidUserIds) {
      try {
        await request({
          method: 'POST',
          url: 'http://localhost:8378/1/loginAs',
          headers: {
            'X-Parse-Application-Id': Parse.applicationId,
            'X-Parse-REST-API-Key': 'rest',
            'X-Parse-Master-Key': 'test',
          },
          body: {
            userId: invalidUserId,
          },
        });

        fail('Request should fail without a valid user ID');
        done();
      } catch (err) {
        expect(err.data.code).toBe(Parse.Error.INVALID_VALUE);
        expect(err.data.error).toBe('userId must not be empty, null, or undefined');
      }

      const sessionsQuery = new Parse.Query(Parse.Session);
      const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
      expect(sessionsAfterRequest.length).toBe(0);
    }

    done();
  });

  it('rejects creating a session for another user without the master key', async done => {
    await Parse.User.signUp('some_user', 'some_password');
    const userId = Parse.User.current().id;
    await Parse.User.logOut();

    try {
      await request({
        method: 'POST',
        url: 'http://localhost:8378/1/loginAs',
        headers: {
          'X-Parse-Application-Id': Parse.applicationId,
          'X-Parse-REST-API-Key': 'rest',
        },
        body: {
          userId,
        },
      });

      fail('Request should fail without the master key');
      done();
    } catch (err) {
      expect(err.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
      expect(err.data.error).toBe('master key is required');
    }

    const sessionsQuery = new Parse.Query(Parse.Session);
    const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
    expect(sessionsAfterRequest.length).toBe(0);

    done();
  });
});

describe('allowClientClassCreation option', () => {
  it('should enforce boolean values', async () => {
    const options = [[], 'a', '', 0, 1, {}, 'true', 'false'];
    for (const option of options) {
      await expectAsync(reconfigureServer({ allowClientClassCreation: option })).toBeRejected();
    }
  });

  it('should accept true value', async () => {
    await reconfigureServer({ allowClientClassCreation: true });
    expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(true);
  });

  it('should accept false value', async () => {
    await reconfigureServer({ allowClientClassCreation: false });
    expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(false);
  });

  it('should default false', async () => {
    // remove predefined allowClientClassCreation:true on global defaultConfiguration
    delete defaultConfiguration.allowClientClassCreation;
    await reconfigureServer(defaultConfiguration);
    expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(false);
    // Need to set it back to true to avoid other test fails
    defaultConfiguration.allowClientClassCreation = true;
  });
});
