'use strict';

const request = require('../lib/request');

describe('Password Policy: ', () => {
  it_id('b400a867-9f05-496f-af79-933aa588dde5')(it)('should show the invalid link page if the user clicks on the password reset link after the token expires', done => {
    const user = new Parse.User();
    let sendEmailOptions;
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        sendEmailOptions = options;
      },
      sendMail: () => {},
    };
    reconfigureServer({
      appName: 'passwordPolicy',
      emailAdapter: emailAdapter,
      passwordPolicy: {
        resetTokenValidityDuration: 0.5, // 0.5 second
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        user.setUsername('testResetTokenValidity');
        user.setPassword('original');
        user.set('email', 'user@parse.com');
        return user.signUp();
      })
      .then(() => {
        Parse.User.requestPasswordReset('user@parse.com').catch(err => {
          jfail(err);
          fail('Reset password request should not fail');
          done();
        });
      })
      .then(() => {
        // wait for a bit more than the validity duration set
        setTimeout(() => {
          expect(sendEmailOptions).not.toBeUndefined();

          request({
            url: sendEmailOptions.link,
            followRedirects: false,
            simple: false,
            resolveWithFullResponse: true,
          })
            .then(response => {
              expect(response.status).toEqual(200);
              expect(response.text).toContain('Invalid password reset link!');
              done();
            })
            .catch(error => {
              fail(error);
            });
        }, 1000);
      })
      .catch(err => {
        jfail(err);
        done();
      });
  });

  it('should show the reset password page if the user clicks on the password reset link before the token expires', done => {
    const user = new Parse.User();
    let sendEmailOptions;
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        sendEmailOptions = options;
      },
      sendMail: () => {},
    };
    reconfigureServer({
      appName: 'passwordPolicy',
      emailAdapter: emailAdapter,
      passwordPolicy: {
        resetTokenValidityDuration: 5, // 5 seconds
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        user.setUsername('testResetTokenValidity');
        user.setPassword('original');
        user.set('email', 'user@parse.com');
        return user.signUp();
      })
      .then(() => {
        Parse.User.requestPasswordReset('user@parse.com').catch(err => {
          jfail(err);
          fail('Reset password request should not fail');
          done();
        });
      })
      .then(() => {
        // wait for a bit but less than the validity duration
        setTimeout(() => {
          expect(sendEmailOptions).not.toBeUndefined();

          request({
            url: sendEmailOptions.link,
            simple: false,
            resolveWithFullResponse: true,
            followRedirects: false,
          })
            .then(response => {
              expect(response.status).toEqual(200);
              expect(response.text).toContain('password');
              done();
            })
            .catch(error => {
              fail(error);
            });
        }, 1000);
      })
      .catch(err => {
        jfail(err);
        done();
      });
  });

  it('should not keep reset token by default', async done => {
    const sendEmailOptions = [];
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        sendEmailOptions.push(options);
      },
      sendMail: () => {},
    };
    await reconfigureServer({
      appName: 'passwordPolicy',
      emailAdapter: emailAdapter,
      passwordPolicy: {
        resetTokenValidityDuration: 5 * 60, // 5 minutes
      },
      publicServerURL: 'http://localhost:8378/1',
    });
    const user = new Parse.User();
    user.setUsername('testResetTokenValidity');
    user.setPassword('original');
    user.set('email', 'user@example.com');
    await user.signUp();
    await Parse.User.requestPasswordReset('user@example.com');
    await Parse.User.requestPasswordReset('user@example.com');
    expect(sendEmailOptions[0].link).not.toBe(sendEmailOptions[1].link);
    done();
  });

  it_id('7d98e1f2-ae89-4038-9ea7-5254854ea42e')(it)('should keep reset token with resetTokenReuseIfValid', async done => {
    const sendEmailOptions = [];
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        sendEmailOptions.push(options);
      },
      sendMail: () => {},
    };
    await reconfigureServer({
      appName: 'passwordPolicy',
      emailAdapter: emailAdapter,
      passwordPolicy: {
        resetTokenValidityDuration: 5 * 60, // 5 minutes
        resetTokenReuseIfValid: true,
      },
      publicServerURL: 'http://localhost:8378/1',
    });
    const user = new Parse.User();
    user.setUsername('testResetTokenValidity');
    user.setPassword('original');
    user.set('email', 'user@example.com');
    await user.signUp();
    await Parse.User.requestPasswordReset('user@example.com');
    await Parse.User.requestPasswordReset('user@example.com');
    expect(sendEmailOptions[0].link).toBe(sendEmailOptions[1].link);
    done();
  });

  it('should throw with invalid resetTokenReuseIfValid', async done => {
    const sendEmailOptions = [];
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        sendEmailOptions.push(options);
      },
      sendMail: () => {},
    };
    try {
      await reconfigureServer({
        appName: 'passwordPolicy',
        emailAdapter: emailAdapter,
        passwordPolicy: {
          resetTokenValidityDuration: 5 * 60, // 5 minutes
          resetTokenReuseIfValid: [],
        },
        publicServerURL: 'http://localhost:8378/1',
      });
      fail('should have thrown.');
    } catch (e) {
      expect(e).toBe('resetTokenReuseIfValid must be a boolean value');
    }
    try {
      await reconfigureServer({
        appName: 'passwordPolicy',
        emailAdapter: emailAdapter,
        passwordPolicy: {
          resetTokenReuseIfValid: true,
        },
        publicServerURL: 'http://localhost:8378/1',
      });
      fail('should have thrown.');
    } catch (e) {
      expect(e).toBe('You cannot use resetTokenReuseIfValid without resetTokenValidityDuration');
    }
    done();
  });

  it('should fail if passwordPolicy.resetTokenValidityDuration is not a number', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        resetTokenValidityDuration: 'not a number',
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.resetTokenValidityDuration "not a number" test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.resetTokenValidityDuration must be a positive number');
        done();
      });
  });

  it('should fail if passwordPolicy.resetTokenValidityDuration is zero or a negative number', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        resetTokenValidityDuration: 0,
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('resetTokenValidityDuration negative number test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.resetTokenValidityDuration must be a positive number');
        done();
      });
  });

  it('should fail if passwordPolicy.validatorPattern setting is invalid type', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: 1234, // number is not a valid setting
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.validatorPattern type test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual(
          'passwordPolicy.validatorPattern must be a regex string or RegExp object.'
        );
        done();
      });
  });

  it('should fail if passwordPolicy.validatorCallback setting is invalid type', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorCallback: 'abc', // string is not a valid setting
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.validatorCallback type test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.validatorCallback must be a function.');
        done();
      });
  });

  it('signup should fail if password does not conform to the policy enforced using validatorPattern', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: /[0-9]+/, // password should contain at least one digit
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('nodigit');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          fail('Should have failed as password does not conform to the policy.');
          done();
        })
        .catch(error => {
          expect(error.code).toEqual(142);
          done();
        });
    });
  });

  it('signup should fail if password does not conform to the policy enforced using validatorPattern string', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: '^.{8,}', // password should contain at least 8 char
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('less');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          fail('Should have failed as password does not conform to the policy.');
          done();
        })
        .catch(error => {
          expect(error.code).toEqual(142);
          done();
        });
    });
  });

  it('signup should fail if password is empty', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: '^.{8,}', // password should contain at least 8 char
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          fail('Should have failed as password does not conform to the policy.');
          done();
        })
        .catch(error => {
          expect(error.message).toEqual('Cannot sign up user with an empty password.');
          done();
        });
    });
  });

  it('signup should succeed if password conforms to the policy enforced using validatorPattern', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: /[0-9]+/, // password should contain at least one digit
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('1digit');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.logOut()
            .then(() => {
              Parse.User.logIn('user1', '1digit')
                .then(function () {
                  done();
                })
                .catch(err => {
                  jfail(err);
                  fail('Should be able to login');
                  done();
                });
            })
            .catch(error => {
              jfail(error);
              fail('logout should have succeeded');
              done();
            });
        })
        .catch(error => {
          jfail(error);
          fail('Signup should have succeeded as password conforms to the policy.');
          done();
        });
    });
  });

  it('signup should succeed if password conforms to the policy enforced using validatorPattern string', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: '[!@#$]+', // password should contain at least one special char
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('p@sswrod');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.logOut()
            .then(() => {
              Parse.User.logIn('user1', 'p@sswrod')
                .then(function () {
                  done();
                })
                .catch(err => {
                  jfail(err);
                  fail('Should be able to login');
                  done();
                });
            })
            .catch(error => {
              jfail(error);
              fail('logout should have succeeded');
              done();
            });
        })
        .catch(error => {
          jfail(error);
          fail('Signup should have succeeded as password conforms to the policy.');
          done();
        });
    });
  });

  it('signup should fail if password does not conform to the policy enforced using validatorCallback', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorCallback: () => false, // just fail
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('any');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          fail('Should have failed as password does not conform to the policy.');
          done();
        })
        .catch(error => {
          expect(error.code).toEqual(142);
          done();
        });
    });
  });

  it('signup should succeed if password conforms to the policy enforced using validatorCallback', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorCallback: () => true, // never fail
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('oneUpper');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.logOut()
            .then(() => {
              Parse.User.logIn('user1', 'oneUpper')
                .then(function () {
                  done();
                })
                .catch(err => {
                  jfail(err);
                  fail('Should be able to login');
                  done();
                });
            })
            .catch(error => {
              jfail(error);
              fail('Logout should have succeeded');
              done();
            });
        })
        .catch(error => {
          jfail(error);
          fail('Should have succeeded as password conforms to the policy.');
          done();
        });
    });
  });

  it('signup should fail if password does not match validatorPattern but succeeds validatorCallback', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter
        validatorCallback: () => true,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('all lower');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          fail('Should have failed as password does not conform to the policy.');
          done();
        })
        .catch(error => {
          expect(error.code).toEqual(142);
          done();
        });
    });
  });

  it('signup should fail if password matches validatorPattern but fails validatorCallback', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter
        validatorCallback: () => false,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('oneUpper');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          fail('Should have failed as password does not conform to the policy.');
          done();
        })
        .catch(error => {
          expect(error.code).toEqual(142);
          done();
        });
    });
  });

  it('signup should succeed if password conforms to both validatorPattern and validatorCallback', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: /[A-Z]+/, // password should contain at least one digit
        validatorCallback: () => true,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('oneUpper');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.logOut()
            .then(() => {
              Parse.User.logIn('user1', 'oneUpper')
                .then(function () {
                  done();
                })
                .catch(err => {
                  jfail(err);
                  fail('Should be able to login');
                  done();
                });
            })
            .catch(error => {
              jfail(error);
              fail('logout should have succeeded');
              done();
            });
        })
        .catch(error => {
          jfail(error);
          fail('Should have succeeded as password conforms to the policy.');
          done();
        });
    });
  });

  it('should reset password if new password conforms to password policy', done => {
    const user = new Parse.User();
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        request({
          url: options.link,
          followRedirects: false,
          simple: false,
          resolveWithFullResponse: true,
        })
          .then(response => {
            expect(response.status).toEqual(200);
            const re = /name="token"[^>]*value="([^"]+)"/;
            const match = response.text.match(re);
            if (!match) {
              fail('should have a token');
              done();
              return;
            }
            const token = match[1];

            request({
              method: 'POST',
              url: 'http://localhost:8378/1/apps/test/request_password_reset',
              body: `new_password=has2init&token=${token}`,
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
              },
              followRedirects: false,
              simple: false,
              resolveWithFullResponse: true,
            })
              .then(response => {
                expect(response.status).toEqual(200);
                expect(response.text).toContain('Success!');

                Parse.User.logIn('user1', 'has2init')
                  .then(function () {
                    done();
                  })
                  .catch(err => {
                    jfail(err);
                    fail('should login with new password');
                    done();
                  });
              })
              .catch(error => {
                jfail(error);
                fail('Failed to POST request password reset');
                done();
              });
          })
          .catch(error => {
            jfail(error);
            fail('Failed to get the reset link');
            done();
          });
      },
      sendMail: () => {},
    };
    reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      emailAdapter: emailAdapter,
      passwordPolicy: {
        validatorPattern: /[0-9]+/, // password should contain at least one digit
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('has 1 digit');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
            jfail(err);
            fail('Reset password request should not fail');
            done();
          });
        })
        .catch(error => {
          jfail(error);
          fail('signUp should not fail');
          done();
        });
    });
  });

  it('should fail to reset password if the new password does not conform to password policy', done => {
    const user = new Parse.User();
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        request({
          url: options.link,
          followRedirects: false,
          simple: false,
          resolveWithFullResponse: true,
        })
          .then(response => {
            expect(response.status).toEqual(200);
            const re = /name="token"[^>]*value="([^"]+)"/;
            const match = response.text.match(re);
            if (!match) {
              fail('should have a token');
              done();
              return;
            }
            const token = match[1];

            request({
              method: 'POST',
              url: 'http://localhost:8378/1/apps/test/request_password_reset',
              body: `new_password=hasnodigit&token=${token}`,
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
              },
              followRedirects: false,
              simple: false,
              resolveWithFullResponse: true,
            })
              .then(response => {
                expect(response.status).toEqual(200);
                expect(response.text).toContain('Password should contain at least one digit.');

                Parse.User.logIn('user1', 'has 1 digit')
                  .then(function () {
                    done();
                  })
                  .catch(err => {
                    jfail(err);
                    fail('should login with old password');
                    done();
                  });
              })
              .catch(error => {
                jfail(error);
                fail('Failed to POST request password reset');
                done();
              });
          })
          .catch(error => {
            jfail(error);
            fail('Failed to get the reset link');
            done();
          });
      },
      sendMail: () => {},
    };
    reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      emailAdapter: emailAdapter,
      passwordPolicy: {
        validatorPattern: /[0-9]+/, // password should contain at least one digit
        validationError: 'Password should contain at least one digit.',
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('has 1 digit');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
            jfail(err);
            fail('Reset password request should not fail');
            done();
          });
        })
        .catch(error => {
          jfail(error);
          fail('signUp should not fail');
          done();
        });
    });
  });

  it('should fail if passwordPolicy.doNotAllowUsername is not a boolean value', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        doNotAllowUsername: 'no',
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.doNotAllowUsername type test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.doNotAllowUsername must be a boolean value.');
        done();
      });
  });

  it('signup should fail if password contains the username and is not allowed by policy', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: /[0-9]+/,
        doNotAllowUsername: true,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('@user11');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          fail('Should have failed as password contains username.');
          done();
        })
        .catch(error => {
          expect(error.code).toEqual(142);
          expect(error.message).toEqual('Password cannot contain your username.');
          done();
        });
    });
  });

  it('signup should succeed if password does not contain the username and is not allowed by policy', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        doNotAllowUsername: true,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('r@nd0m');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          done();
        })
        .catch(() => {
          fail('Should have succeeded as password does not contain username.');
          done();
        });
    });
  });

  it('signup should succeed if password contains the username and it is allowed by policy', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        validatorPattern: /[0-9]+/,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          done();
        })
        .catch(() => {
          fail('Should have succeeded as policy allows username in password.');
          done();
        });
    });
  });

  it('should fail to reset password if the new password contains username and not allowed by password policy', done => {
    const user = new Parse.User();
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        request({
          url: options.link,
          followRedirects: false,
          simple: false,
          resolveWithFullResponse: true,
        })
          .then(response => {
            expect(response.status).toEqual(200);
            const re = /name="token"[^>]*value="([^"]+)"/;
            const match = response.text.match(re);
            if (!match) {
              fail('should have a token');
              done();
              return;
            }
            const token = match[1];

            request({
              method: 'POST',
              url: 'http://localhost:8378/1/apps/test/request_password_reset',
              body: `new_password=xuser12&token=${token}`,
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
              },
              followRedirects: false,
              simple: false,
              resolveWithFullResponse: true,
            })
              .then(response => {
                expect(response.status).toEqual(200);
                expect(response.text).toContain('Password cannot contain your username.');

                Parse.User.logIn('user1', 'r@nd0m')
                  .then(function () {
                    done();
                  })
                  .catch(err => {
                    jfail(err);
                    fail('should login with old password');
                    done();
                  });
              })
              .catch(error => {
                jfail(error);
                fail('Failed to POST request password reset');
                done();
              });
          })
          .catch(error => {
            jfail(error);
            fail('Failed to get the reset link');
            done();
          });
      },
      sendMail: () => {},
    };
    reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      emailAdapter: emailAdapter,
      passwordPolicy: {
        doNotAllowUsername: true,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('r@nd0m');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
            jfail(err);
            fail('Reset password request should not fail');
            done();
          });
        })
        .catch(error => {
          jfail(error);
          fail('signUp should not fail');
          done();
        });
    });
  });

  it('Should return error when password violates Password Policy and reset through ajax', async done => {
    const user = new Parse.User();
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: async options => {
        const response = await request({
          url: options.link,
          followRedirects: false,
          simple: false,
          resolveWithFullResponse: true,
        });
        expect(response.status).toEqual(200);
        const re = /name="token"[^>]*value="([^"]+)"/;
        const match = response.text.match(re);
        if (!match) {
          fail('should have a token');
          return;
        }
        const token = match[1];

        try {
          await request({
            method: 'POST',
            url: 'http://localhost:8378/1/apps/test/request_password_reset',
            body: `new_password=xuser12&token=${token}`,
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded',
              'X-Requested-With': 'XMLHttpRequest',
            },
            followRedirects: false,
          });
        } catch (error) {
          expect(error.status).not.toBe(302);
          expect(error.text).toEqual(
            '{"code":-1,"error":"Password cannot contain your username."}'
          );
        }
        await Parse.User.logIn('user1', 'r@nd0m');
        done();
      },
      sendMail: () => {},
    };
    await reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      emailAdapter: emailAdapter,
      passwordPolicy: {
        doNotAllowUsername: true,
      },
      publicServerURL: 'http://localhost:8378/1',
    });
    user.setUsername('user1');
    user.setPassword('r@nd0m');
    user.set('email', 'user1@parse.com');
    await user.signUp();

    await Parse.User.requestPasswordReset('user1@parse.com');
  });

  it('should reset password even if the new password contains user name while the policy allows', done => {
    const user = new Parse.User();
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        request({
          url: options.link,
          followRedirects: false,
          simple: false,
          resolveWithFullResponse: true,
        })
          .then(response => {
            expect(response.status).toEqual(200);
            const re = /name="token"[^>]*value="([^"]+)"/;
            const match = response.text.match(re);
            if (!match) {
              fail('should have a token');
              done();
              return;
            }
            const token = match[1];

            request({
              method: 'POST',
              url: 'http://localhost:8378/1/apps/test/request_password_reset',
              body: `new_password=uuser11&token=${token}`,
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
              },
              followRedirects: false,
              simple: false,
              resolveWithFullResponse: true,
            })
              .then(response => {
                expect(response.status).toEqual(200);
                expect(response.text).toContain('Success!');

                Parse.User.logIn('user1', 'uuser11')
                  .then(function () {
                    done();
                  })
                  .catch(err => {
                    jfail(err);
                    fail('should login with new password');
                    done();
                  });
              })
              .catch(error => {
                jfail(error);
                fail('Failed to POST request password reset');
              });
          })
          .catch(error => {
            jfail(error);
            fail('Failed to get the reset link');
          });
      },
      sendMail: () => {},
    };
    reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      emailAdapter: emailAdapter,
      passwordPolicy: {
        validatorPattern: /[0-9]+/,
        doNotAllowUsername: false,
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        user.setUsername('user1');
        user.setPassword('has 1 digit');
        user.set('email', 'user1@parse.com');
        user.signUp().then(() => {
          Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
            jfail(err);
            fail('Reset password request should not fail');
            done();
          });
        });
      })
      .catch(error => {
        jfail(error);
        fail('signUp should not fail');
        done();
      });
  });

  it('should fail if passwordPolicy.maxPasswordAge is not a number', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        maxPasswordAge: 'not a number',
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.maxPasswordAge "not a number" test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.maxPasswordAge must be a positive number');
        done();
      });
  });

  it('should fail if passwordPolicy.maxPasswordAge is a negative number', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        maxPasswordAge: -100,
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.maxPasswordAge negative number test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.maxPasswordAge must be a positive number');
        done();
      });
  });

  it_id('d7d0a93e-efe6-48c0-b622-0f7fb570ccc1')(it)('should succeed if logged in before password expires', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        maxPasswordAge: 1, // 1 day
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.logIn('user1', 'user1')
            .then(() => {
              done();
            })
            .catch(error => {
              jfail(error);
              fail('Login should have succeeded before password expiry.');
              done();
            });
        })
        .catch(error => {
          jfail(error);
          fail('Signup failed.');
          done();
        });
    });
  });

  it_id('22428408-8763-445d-9833-2b2d92008f62')(it)('should fail if logged in after password expires', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          // wait for a bit more than the validity duration set
          setTimeout(() => {
            Parse.User.logIn('user1', 'user1')
              .then(() => {
                fail('logIn should have failed');
                done();
              })
              .catch(error => {
                expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
                expect(error.message).toEqual(
                  'Your password has expired. Please reset your password.'
                );
                done();
              });
          }, 1000);
        })
        .catch(error => {
          jfail(error);
          fail('Signup failed.');
          done();
        });
    });
  });

  it_id('cc97a109-e35f-4f94-b942-3a6134921cdd')(it)('should apply password expiry policy to existing user upon first login after policy is enabled', done => {
    const user = new Parse.User();
    reconfigureServer({
      appName: 'passwordPolicy',
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          Parse.User.logOut()
            .then(() => {
              reconfigureServer({
                appName: 'passwordPolicy',
                passwordPolicy: {
                  maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec
                },
                publicServerURL: 'http://localhost:8378/1',
              }).then(() => {
                Parse.User.logIn('user1', 'user1')
                  .then(() => {
                    Parse.User.logOut()
                      .then(() => {
                        // wait for a bit more than the validity duration set
                        setTimeout(() => {
                          Parse.User.logIn('user1', 'user1')
                            .then(() => {
                              fail('logIn should have failed');
                              done();
                            })
                            .catch(error => {
                              expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
                              expect(error.message).toEqual(
                                'Your password has expired. Please reset your password.'
                              );
                              done();
                            });
                        }, 2000);
                      })
                      .catch(error => {
                        jfail(error);
                        fail('logout should have succeeded');
                        done();
                      });
                  })
                  .catch(error => {
                    jfail(error);
                    fail('Login failed.');
                    done();
                  });
              });
            })
            .catch(error => {
              jfail(error);
              fail('logout should have succeeded');
              done();
            });
        })
        .catch(error => {
          jfail(error);
          fail('Signup failed.');
          done();
        });
    });
  });

  it_id('d1e6ab9d-c091-4fea-b952-08b7484bfc89')(it)('should reset password timestamp when password is reset', done => {
    const user = new Parse.User();
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        request({
          url: options.link,
          followRedirects: false,
          simple: false,
          resolveWithFullResponse: true,
        })
          .then(response => {
            expect(response.status).toEqual(200);
            const re = /name="token"[^>]*value="([^"]+)"/;
            const match = response.text.match(re);
            if (!match) {
              fail('should have a token');
              done();
              return;
            }
            const token = match[1];

            request({
              method: 'POST',
              url: 'http://localhost:8378/1/apps/test/request_password_reset',
              body: `new_password=uuser11&token=${token}`,
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
              },
              followRedirects: false,
              simple: false,
              resolveWithFullResponse: true,
            })
              .then(response => {
                expect(response.status).toEqual(200);
                expect(response.text).toContain('Success!');

                Parse.User.logIn('user1', 'uuser11')
                  .then(function () {
                    done();
                  })
                  .catch(err => {
                    jfail(err);
                    fail('should login with new password');
                    done();
                  });
              })
              .catch(error => {
                jfail(error);
                fail('Failed to POST request password reset');
              });
          })
          .catch(error => {
            jfail(error);
            fail('Failed to get the reset link');
          });
      },
      sendMail: () => {},
    };
    reconfigureServer({
      appName: 'passwordPolicy',
      emailAdapter: emailAdapter,
      passwordPolicy: {
        maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          // wait for a bit more than the validity duration set
          setTimeout(() => {
            Parse.User.logIn('user1', 'user1')
              .then(() => {
                fail('logIn should have failed');
                done();
              })
              .catch(error => {
                expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
                expect(error.message).toEqual(
                  'Your password has expired. Please reset your password.'
                );
                Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
                  jfail(err);
                  fail('Reset password request should not fail');
                  done();
                });
              });
          }, 1000);
        })
        .catch(error => {
          jfail(error);
          fail('Signup failed.');
          done();
        });
    });
  });

  it('should fail if passwordPolicy.maxPasswordHistory is not a number', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        maxPasswordHistory: 'not a number',
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.maxPasswordHistory "not a number" test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20');
        done();
      });
  });

  it('should fail if passwordPolicy.maxPasswordHistory is a negative number', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        maxPasswordHistory: -10,
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.maxPasswordHistory negative number test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20');
        done();
      });
  });

  it('should fail if passwordPolicy.maxPasswordHistory is greater than 20', done => {
    reconfigureServer({
      appName: 'passwordPolicy',
      passwordPolicy: {
        maxPasswordHistory: 21,
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        fail('passwordPolicy.maxPasswordHistory negative number test failed');
        done();
      })
      .catch(err => {
        expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20');
        done();
      });
  });

  it('should fail to reset if the new password is same as the last password', done => {
    const user = new Parse.User();
    const emailAdapter = {
      sendVerificationEmail: () => Promise.resolve(),
      sendPasswordResetEmail: options => {
        request({
          url: options.link,
          followRedirects: false,
        })
          .then(response => {
            expect(response.status).toEqual(200);
            const re = /name="token"[^>]*value="([^"]+)"/;
            const match = response.text.match(re);
            if (!match) {
              fail('should have a token');
              return Promise.reject('Invalid password link');
            }
            return Promise.resolve(match[1]); // token
          })
          .then(token => {
            return request({
              method: 'POST',
              url: 'http://localhost:8378/1/apps/test/request_password_reset',
              body: `new_password=user1&token=${token}`,
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
              },
              followRedirects: false,
              simple: false,
              resolveWithFullResponse: true,
            }).then(response => {
              return [response, token];
            });
          })
          .then(data => {
            const response = data[0];
            const token = data[1];
            expect(response.status).toEqual(200);
            expect(response.text).toContain('New password should not be the same as last 1 passwords.');
            done();
            return Promise.resolve();
          })
          .catch(error => {
            fail(error);
            fail('Repeat password test failed');
            done();
          });
      },
      sendMail: () => {},
    };
    reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      emailAdapter: emailAdapter,
      passwordPolicy: {
        maxPasswordHistory: 1,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          return Parse.User.logOut();
        })
        .then(() => {
          return Parse.User.requestPasswordReset('user1@parse.com');
        })
        .catch(error => {
          jfail(error);
          fail('SignUp or reset request failed');
          done();
        });
    });
  });

  it('should fail if the new password is same as the previous one', done => {
    const user = new Parse.User();

    reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      passwordPolicy: {
        maxPasswordHistory: 5,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          // try to set the same password as the previous one
          user.setPassword('user1');
          return user.save();
        })
        .then(() => {
          fail('should have failed because the new password is same as the old');
          done();
        })
        .catch(error => {
          expect(error.message).toEqual('New password should not be the same as last 5 passwords.');
          expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
          done();
        });
    });
  });

  it('should fail if the new password is same as the 5th oldest one and policy does not allow the previous 5', done => {
    const user = new Parse.User();

    reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      passwordPolicy: {
        maxPasswordHistory: 5,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          // build history
          user.setPassword('user2');
          return user.save();
        })
        .then(() => {
          user.setPassword('user3');
          return user.save();
        })
        .then(() => {
          user.setPassword('user4');
          return user.save();
        })
        .then(() => {
          user.setPassword('user5');
          return user.save();
        })
        .then(() => {
          // set the same password as the initial one
          user.setPassword('user1');
          return user.save();
        })
        .then(() => {
          fail('should have failed because the new password is same as the old');
          done();
        })
        .catch(error => {
          expect(error.message).toEqual('New password should not be the same as last 5 passwords.');
          expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
          done();
        });
    });
  });

  it('should succeed if the new password is same as the 6th oldest one and policy does not allow only previous 5', done => {
    const user = new Parse.User();

    reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      passwordPolicy: {
        maxPasswordHistory: 5,
      },
      publicServerURL: 'http://localhost:8378/1',
    }).then(() => {
      user.setUsername('user1');
      user.setPassword('user1');
      user.set('email', 'user1@parse.com');
      user
        .signUp()
        .then(() => {
          // build history
          user.setPassword('user2');
          return user.save();
        })
        .then(() => {
          user.setPassword('user3');
          return user.save();
        })
        .then(() => {
          user.setPassword('user4');
          return user.save();
        })
        .then(() => {
          user.setPassword('user5');
          return user.save();
        })
        .then(() => {
          user.setPassword('user6'); // this pushes initial password out of history
          return user.save();
        })
        .then(() => {
          // set the same password as the initial one
          user.setPassword('user1');
          return user.save();
        })
        .then(() => {
          done();
        })
        .catch(() => {
          fail('should have succeeded because the new password is not in history');
          done();
        });
    });
  });

  it('should not infinitely loop if maxPasswordHistory is 1 (#4918)', async () => {
    const headers = {
      'X-Parse-Application-Id': 'test',
      'X-Parse-Rest-API-Key': 'test',
      'X-Parse-Maintenance-Key': 'test2',
      'Content-Type': 'application/json',
    };
    const user = new Parse.User();
    const query = new Parse.Query(Parse.User);

    await reconfigureServer({
      appName: 'passwordPolicy',
      verifyUserEmails: false,
      maintenanceKey: 'test2',
      passwordPolicy: {
        maxPasswordHistory: 1,
      },
      publicServerURL: 'http://localhost:8378/1',
    });
    user.setUsername('user1');
    user.setPassword('user1');
    user.set('email', 'user1@parse.com');
    await user.signUp();

    user.setPassword('user2');
    await user.save();

    const user1 = await query.get(user.id, { useMasterKey: true });
    expect(user1.get('_password_history')).toBeUndefined();

    const result1 = await request({
      method: 'GET',
      url: `http://localhost:8378/1/classes/_User/${user.id}`,
      json: true,
      headers,
    }).then(res => res.data);
    expect(result1._password_history.length).toBe(1);

    user.setPassword('user3');
    await user.save();

    const result2 = await request({
      method: 'GET',
      url: `http://localhost:8378/1/classes/_User/${user.id}`,
      json: true,
      headers,
    }).then(res => res.data);
    expect(result2._password_history.length).toBe(1);

    expect(result1._password_history).not.toEqual(result2._password_history);
  });
});
