Skip to content
This repository has been archived by the owner on Dec 25, 2024. It is now read-only.

Commit

Permalink
Highlight error fields in change password form (#727)
Browse files Browse the repository at this point in the history
* Highlight error fields in change password form

Other Changes:
- Password strength failure in change password will now report the newPasswordConfirm field in addition to the newPassword field in its response
- Notifications in change password page will now clear before a new notification is displayed

* Have password strength errors share the same error ID
  • Loading branch information
Coteh authored Oct 16, 2023
1 parent bb85239 commit 440dad3
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 41 deletions.
240 changes: 240 additions & 0 deletions cypress/e2e/6-settings/change-password.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,34 @@ describe("change password", () => {
cy.get("input[name='oldPassword']").type(password);
cy.get("input[name='newPassword']").type(newPassword);
cy.get("input[name='newPasswordConfirm']").type(newPassword);

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 200);

// TODO assert using an success code or some other form of success ID instead
cy.assertSuccessMessageContains("Password changed");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.logout();
cy.login(username, newPassword).its("status").should("eq", 200);
});
Expand All @@ -32,13 +53,34 @@ describe("change password", () => {

cy.get("input[name='newPassword']").type(newPassword);
cy.get("input[name='newPasswordConfirm']").type(newPassword);

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 422);

// TODO assert using an error code or some other form of error ID instead
cy.assertErrorMessageContains("Could not change password. Missing old password.");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.logout();
cy.login(username, password).its("status").should("eq", 200);
});
Expand All @@ -48,13 +90,34 @@ describe("change password", () => {
cy.get("input[name='oldPassword']").type("wrong");
cy.get("input[name='newPassword']").type(newPassword);
cy.get("input[name='newPasswordConfirm']").type(newPassword);

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 400);

// TODO assert using an error code or some other form of error ID instead
cy.assertErrorMessageContains("Could not change password. Old password is not correct.");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.logout();
cy.login(username, password).its("status").should("eq", 200);
});
Expand All @@ -63,13 +126,34 @@ describe("change password", () => {

cy.get("input[name='oldPassword']").type(password);
cy.get("input[name='newPasswordConfirm']").type(newPassword);

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 422);

// TODO assert using an error code or some other form of error ID instead
cy.assertErrorMessageContains("Could not change password. Missing new password.");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.logout();
cy.login(username, password).its("status").should("eq", 200);
});
Expand All @@ -78,13 +162,34 @@ describe("change password", () => {

cy.get("input[name='oldPassword']").type(password);
cy.get("input[name='newPassword']").type(newPassword);

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 422);

// TODO assert using an error code or some other form of error ID instead
cy.assertErrorMessageContains("Could not change password. Missing new password confirmation.");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});

cy.logout();
cy.login(username, password).its("status").should("eq", 200);
});
Expand All @@ -94,13 +199,75 @@ describe("change password", () => {
cy.get("input[name='oldPassword']").type(password);
cy.get("input[name='newPassword']").type(newPassword);
cy.get("input[name='newPasswordConfirm']").type("wrong");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 400);

// TODO assert using an error code or some other form of error ID instead
cy.assertErrorMessageContains("Could not change password. Passwords don't match.");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});

cy.logout();
cy.login(username, password).its("status").should("eq", 200);
});
it("should not change user password if new password is not strong enough", () => {
cy.intercept("POST", "/settings/change_password").as("changePassword");

const newWeakPassword = "weak";

cy.get("input[name='oldPassword']").type(password);
cy.get("input[name='newPassword']").type(newWeakPassword);
cy.get("input[name='newPasswordConfirm']").type(newWeakPassword);

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").then((interception) => {
const response = interception.response;

expect(response.statusCode).to.eql(400);
expect(response.body.errorID).to.eql("passwordNotStrong");
});

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});

cy.logout();
cy.login(username, password).its("status").should("eq", 200);
});
Expand Down Expand Up @@ -145,4 +312,77 @@ describe("change password", () => {
cy.login(username, password).its("status").should("eq", 401);
cy.login(username, newPassword).its("status").should("eq", 401);
});
it("should clear notifications and input fields when attempting to change password multiple times", () => {
cy.intercept("POST", "/settings/change_password").as("changePassword");

cy.get("input[name='oldPassword']").type(password);
cy.get("input[name='newPassword']").type(newPassword);
cy.get("input[name='newPasswordConfirm']").type("wrong");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 400);

cy.assertErrorMessageContains("Could not change password. Passwords don't match.");

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 400);

cy.assertErrorMessageContains("Could not change password. Passwords don't match.");
cy.assertErrorMessageNotContains(
"Could not change password. Passwords don't match.Could not change password. Passwords don't match."
);

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return Array.from(el[0].classList).includes("input-field-error");
});

cy.get("input[name='newPasswordConfirm']").clear().type(newPassword);

cy.get(".submit-button").click();

cy.wait("@changePassword").its("response.statusCode").should("eq", 200);

cy.assertSuccessMessageContains("Password changed.");
cy.assertSuccessMessageNotContains(
"Could not change password. Passwords don't match.Could not change password. Passwords don't match.Password changed."
);

cy.get("input[name='oldPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPassword']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
cy.get("input[name='newPasswordConfirm']").should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("input-field-error");
});
});
});
18 changes: 18 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ Cypress.Commands.add("assertErrorMessageContainsMulti", (messages) => {
});
});

Cypress.Commands.add("assertErrorMessageNotContains", (message) => {
cy.get("#notification-overlay-container")
.should("be.visible")
.should("satisfy", (el) => {
return Array.from(el[0].classList).includes("error");
})
.should("not.contain.text", message);
});

Cypress.Commands.add("assertSuccessMessageContains", (message) => {
cy.get("#notification-overlay-container")
.should("be.visible")
Expand All @@ -66,6 +75,15 @@ Cypress.Commands.add("assertSuccessMessageContainsMulti", (messages) => {
});
});

Cypress.Commands.add("assertSuccessMessageNotContains", (message) => {
cy.get("#notification-overlay-container")
.should("be.visible")
.should("satisfy", (el) => {
return !Array.from(el[0].classList).includes("error");
})
.should("not.contain.text", message);
});

Cypress.Commands.add("deleteUser", (username) => {
cy.task("deleteUser", username);
});
Expand Down
2 changes: 1 addition & 1 deletion lib/route/register/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ router.post("/", format("json"), limiter, function (req, res, next) {
return sendError(next, {
statusCode: 400,
message: passwordStrengthResult.errors.join("\n"),
errorID: "weakPassword",
errorID: "passwordNotStrong",
fields: ["password", "passwordConfirm"],
});
}
Expand Down
2 changes: 1 addition & 1 deletion lib/route/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ router.post("/change_password", format("json"), requireUser, limiter, function (
return sendError(next, {
errorID: "passwordNotStrong",
message: passwordStrengthResult.errors.join("\n"),
field: "newPassword",
fields: ["newPassword", "newPasswordConfirm"],
statusCode: 400,
});
}
Expand Down
Loading

0 comments on commit 440dad3

Please sign in to comment.