From cb603328d65a6cfc0d113304d2ef811cb3f87025 Mon Sep 17 00:00:00 2001 From: Jonathan Laperle Date: Fri, 28 Jun 2024 10:45:17 -0400 Subject: [PATCH 1/7] Add reset password route --- client/src/components/Login/LoginForm.vue | 18 +------ .../entry/analysis/modules/ResetPassword.vue | 47 +++++++++++++++++++ client/src/entry/analysis/router.js | 7 +++ 3 files changed, 56 insertions(+), 16 deletions(-) create mode 100644 client/src/entry/analysis/modules/ResetPassword.vue diff --git a/client/src/components/Login/LoginForm.vue b/client/src/components/Login/LoginForm.vue index 4ad09ebb0d57..08988c888f5c 100644 --- a/client/src/components/Login/LoginForm.vue +++ b/client/src/components/Login/LoginForm.vue @@ -97,7 +97,7 @@ async function submitLogin() { } if (response.data.expired_user) { - window.location.href = withPrefix(`/root/login?expired_user=${response.data.expired_user}`); + window.location.href = withPrefix(`/login/start?expired_user=${response.data.expired_user}`); } else if (connectExternalProvider.value) { window.location.href = withPrefix("/user/external_ids?connect_external=true"); } else if (response.data.redirect) { @@ -125,20 +125,6 @@ function setRedirect(url: string) { localStorage.setItem("redirect_url", url); } -async function resetLogin() { - loading.value = true; - try { - const response = await axios.post(withPrefix("/user/reset_password"), { email: login.value }); - messageVariant.value = "info"; - messageText.value = response.data.message; - } catch (e) { - messageVariant.value = "danger"; - messageText.value = errorMessageAsString(e, "Password reset failed for an unknown reason."); - } finally { - loading.value = false; - } -} - function returnToLogin() { router.push("/login/start"); } @@ -205,7 +191,7 @@ function returnToLogin() { v-localize href="javascript:void(0)" role="button" - @click.prevent="resetLogin"> + @click.prevent="router.push('/login/reset_password')"> Click here to reset your password. diff --git a/client/src/entry/analysis/modules/ResetPassword.vue b/client/src/entry/analysis/modules/ResetPassword.vue new file mode 100644 index 000000000000..3078e87e5ac8 --- /dev/null +++ b/client/src/entry/analysis/modules/ResetPassword.vue @@ -0,0 +1,47 @@ + + + diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 43a38292efa2..c681e467e384 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -45,6 +45,7 @@ import Analysis from "entry/analysis/modules/Analysis"; import CenterFrame from "entry/analysis/modules/CenterFrame"; import Home from "entry/analysis/modules/Home"; import Login from "entry/analysis/modules/Login"; +import ResetPassword from "entry/analysis/modules/ResetPassword"; import WorkflowEditorModule from "entry/analysis/modules/WorkflowEditor"; import AdminRoutes from "entry/analysis/routes/admin-routes"; import LibraryRoutes from "entry/analysis/routes/library-routes"; @@ -125,6 +126,12 @@ export function getRouter(Galaxy) { component: Login, redirect: redirectLoggedIn(), }, + /** Login entry route */ + { + path: "/login/reset_password", + component: ResetPassword, + redirect: redirectLoggedIn(), + }, /** Page editor */ { path: "/pages/editor", From 1b604d2288add2b29a01027e6b97f997d419a759 Mon Sep 17 00:00:00 2001 From: Jonathan Laperle Date: Sat, 29 Jun 2024 11:48:46 -0400 Subject: [PATCH 2/7] start building test for reset password route --- .../analysis/modules/ResetPassword.test.ts | 64 +++++++++++++++++++ .../entry/analysis/modules/ResetPassword.vue | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 client/src/entry/analysis/modules/ResetPassword.test.ts diff --git a/client/src/entry/analysis/modules/ResetPassword.test.ts b/client/src/entry/analysis/modules/ResetPassword.test.ts new file mode 100644 index 000000000000..95e4bac64c95 --- /dev/null +++ b/client/src/entry/analysis/modules/ResetPassword.test.ts @@ -0,0 +1,64 @@ +import { createTestingPinia } from "@pinia/testing"; +import { getLocalVue } from "@tests/jest/helpers"; +import { mount } from "@vue/test-utils"; +import { setActivePinia } from "pinia"; + +import { getGalaxyInstance } from "@/app/singleton"; + +import ResetPassword from "./ResetPassword.vue"; + +const localVue = getLocalVue(true); + +const configMock = { + allow_user_creation: true, + enable_oidc: true, + mailing_join_addr: "mailing_join_addr", + prefer_custos_login: true, + registration_warning_message: "registration_warning_message", + server_mail_configured: true, + show_welcome_with_login: true, + terms_url: "terms_url", + welcome_url: "welcome_url", +}; + +jest.mock("app/singleton"); +jest.mock("@/composables/config", () => ({ + useConfig: jest.fn(() => ({ + config: configMock, + + isConfigLoaded: true, + })), +})); + +const mockRouter = (query: object) => ({ + currentRoute: { + query, + }, +}); + +(getGalaxyInstance as jest.Mock).mockReturnValue({ session_csrf_token: "session_csrf_token" }); + +function mountResetPassword(routerQuery: object = {}) { + const pinia = createTestingPinia(); + setActivePinia(pinia); + + return mount(ResetPassword, { + localVue, + pinia, + mocks: { + $router: mockRouter(routerQuery), + }, + }); +} + +describe("ResetPassword", () => { + it("ResetPassword index attribute matching", async () => { + const wrapper = mountResetPassword({ + redirect: "redirect_url", + }); + + console.log("wrapper:", wrapper.html()); + const emailForm = wrapper.find("#reset-email"); + console.log("emailForm:", emailForm.element); + }); +}); diff --git a/client/src/entry/analysis/modules/ResetPassword.vue b/client/src/entry/analysis/modules/ResetPassword.vue index 3078e87e5ac8..1b68f4dc1088 100644 --- a/client/src/entry/analysis/modules/ResetPassword.vue +++ b/client/src/entry/analysis/modules/ResetPassword.vue @@ -36,7 +36,7 @@ async function resetLogin() { - + Reset your password From af457c55af7ee6b34d61d32f9b4e29bceacda9be Mon Sep 17 00:00:00 2001 From: Jonathan Laperle Date: Sat, 29 Jun 2024 14:22:29 -0400 Subject: [PATCH 3/7] use same css as login page --- .../entry/analysis/modules/ResetPassword.vue | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/client/src/entry/analysis/modules/ResetPassword.vue b/client/src/entry/analysis/modules/ResetPassword.vue index 1b68f4dc1088..7df8fee1d611 100644 --- a/client/src/entry/analysis/modules/ResetPassword.vue +++ b/client/src/entry/analysis/modules/ResetPassword.vue @@ -27,21 +27,25 @@ async function resetLogin() { From 1d74be86e054a5cda2152c8a38a53032d7c22bde Mon Sep 17 00:00:00 2001 From: Jonathan Laperle Date: Sat, 29 Jun 2024 14:43:56 -0400 Subject: [PATCH 4/7] pass email from login to password reset route --- client/src/components/Login/LoginForm.vue | 7 ++++++- client/src/entry/analysis/modules/ResetPassword.vue | 5 ++++- client/src/entry/analysis/router.js | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/client/src/components/Login/LoginForm.vue b/client/src/components/Login/LoginForm.vue index 08988c888f5c..c57d496e9cbb 100644 --- a/client/src/components/Login/LoginForm.vue +++ b/client/src/components/Login/LoginForm.vue @@ -191,7 +191,12 @@ function returnToLogin() { v-localize href="javascript:void(0)" role="button" - @click.prevent="router.push('/login/reset_password')"> + @click.prevent=" + router.push({ + path: '/login/reset_password', + query: { email: login }, + }) + "> Click here to reset your password. diff --git a/client/src/entry/analysis/modules/ResetPassword.vue b/client/src/entry/analysis/modules/ResetPassword.vue index 7df8fee1d611..d56b07e6b7b2 100644 --- a/client/src/entry/analysis/modules/ResetPassword.vue +++ b/client/src/entry/analysis/modules/ResetPassword.vue @@ -2,12 +2,15 @@ import axios from "axios"; import { BAlert, BButton, BCard, BForm, BFormGroup, BFormInput } from "bootstrap-vue"; import { ref } from "vue"; +import { useRouter } from "vue-router/composables"; import { withPrefix } from "@/utils/redirect"; import { errorMessageAsString } from "@/utils/simple-error"; +const router = useRouter(); + const loading = ref(false); -const email = ref(""); +const email = ref(router.currentRoute.query.email || ""); const message = ref(""); const messageVariant = ref("info"); diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index c681e467e384..f02004f16171 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -130,6 +130,7 @@ export function getRouter(Galaxy) { { path: "/login/reset_password", component: ResetPassword, + props: (route) => ({ email: route.query.email }), redirect: redirectLoggedIn(), }, /** Page editor */ From fac06e54d944151209b15f757400fcfb8ca1cf7f Mon Sep 17 00:00:00 2001 From: Jonathan Laperle Date: Sat, 29 Jun 2024 14:51:44 -0400 Subject: [PATCH 5/7] progress on ResetPassword test --- .../src/entry/analysis/modules/ResetPassword.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/entry/analysis/modules/ResetPassword.test.ts b/client/src/entry/analysis/modules/ResetPassword.test.ts index 95e4bac64c95..2c82c43839a0 100644 --- a/client/src/entry/analysis/modules/ResetPassword.test.ts +++ b/client/src/entry/analysis/modules/ResetPassword.test.ts @@ -6,6 +6,7 @@ import { setActivePinia } from "pinia"; import { getGalaxyInstance } from "@/app/singleton"; import ResetPassword from "./ResetPassword.vue"; +import assert from "assert"; const localVue = getLocalVue(true); @@ -56,9 +57,13 @@ describe("ResetPassword", () => { const wrapper = mountResetPassword({ redirect: "redirect_url", }); + const emailField = wrapper.find("#reset-email"); + const submitButton = wrapper.find("#reset-password"); + const testEmail = "eihfeuh"; + + emailField.setValue(testEmail); + const emailValue = emailField.element.textContent; + expect(emailValue).toBe(testEmail); - console.log("wrapper:", wrapper.html()); - const emailForm = wrapper.find("#reset-email"); - console.log("emailForm:", emailForm.element); }); }); From 4ca7943e0aa75a232413fdb502f13e3e03749b57 Mon Sep 17 00:00:00 2001 From: Jonathan Laperle Date: Fri, 5 Jul 2024 21:36:54 -0400 Subject: [PATCH 6/7] add tests for ResetPassword --- .../analysis/modules/ResetPassword.test.ts | 87 ++++++++++++++----- .../entry/analysis/modules/ResetPassword.vue | 8 +- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/client/src/entry/analysis/modules/ResetPassword.test.ts b/client/src/entry/analysis/modules/ResetPassword.test.ts index 2c82c43839a0..cae83aa1b421 100644 --- a/client/src/entry/analysis/modules/ResetPassword.test.ts +++ b/client/src/entry/analysis/modules/ResetPassword.test.ts @@ -1,12 +1,9 @@ -import { createTestingPinia } from "@pinia/testing"; import { getLocalVue } from "@tests/jest/helpers"; import { mount } from "@vue/test-utils"; -import { setActivePinia } from "pinia"; - -import { getGalaxyInstance } from "@/app/singleton"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; import ResetPassword from "./ResetPassword.vue"; -import assert from "assert"; const localVue = getLocalVue(true); @@ -26,7 +23,6 @@ jest.mock("app/singleton"); jest.mock("@/composables/config", () => ({ useConfig: jest.fn(() => ({ config: configMock, - isConfigLoaded: true, })), })); @@ -37,15 +33,10 @@ const mockRouter = (query: object) => ({ }, }); -(getGalaxyInstance as jest.Mock).mockReturnValue({ session_csrf_token: "session_csrf_token" }); - function mountResetPassword(routerQuery: object = {}) { - const pinia = createTestingPinia(); - setActivePinia(pinia); - return mount(ResetPassword, { localVue, - pinia, + attachTo: document.body, mocks: { $router: mockRouter(routerQuery), }, @@ -53,17 +44,73 @@ function mountResetPassword(routerQuery: object = {}) { } describe("ResetPassword", () => { - it("ResetPassword index attribute matching", async () => { - const wrapper = mountResetPassword({ - redirect: "redirect_url", - }); + it("query", async () => { + const email = "test"; + const wrapper = mountResetPassword({ email: "test" }); + const emailField = wrapper.find("#reset-email"); + const emailValue = (emailField.element as HTMLInputElement).value; + expect(emailValue).toBe(email); + }); + + it("button text", async () => { + const wrapper = mountResetPassword(); + const submitButton = wrapper.find("#reset-password"); + (expect(submitButton.text()) as any).toBeLocalizationOf("Send password reset email"); + }); + + it("validate email", async () => { + const wrapper = mountResetPassword(); const submitButton = wrapper.find("#reset-password"); - const testEmail = "eihfeuh"; + const emailField = wrapper.find("#reset-email"); + const emailElement = emailField.element as HTMLInputElement; + + let email = ""; + await emailField.setValue(email); + expect(emailElement.value).toBe(email); + await submitButton.trigger("click"); + expect(emailElement.checkValidity()).toBe(false); - emailField.setValue(testEmail); - const emailValue = emailField.element.textContent; - expect(emailValue).toBe(testEmail); + email = "test"; + await emailField.setValue(email); + expect(emailElement.value).toBe(email); + await submitButton.trigger("click"); + expect(emailElement.checkValidity()).toBe(false); + email = "test@test.com"; + await emailField.setValue(email); + expect(emailElement.value).toBe(email); + await submitButton.trigger("click"); + expect(emailElement.checkValidity()).toBe(true); + }); + + it("display success message", async () => { + const wrapper = mountResetPassword({ email: "test@test.com" }); + const mockAxios = new MockAdapter(axios); + const submitButton = wrapper.find("#reset-password"); + + mockAxios.onPost("/user/reset_password").reply(200, { + message: "Reset link has been sent to your email.", + }); + await submitButton.trigger("click"); + setTimeout(async () => { + const alertSuccess = wrapper.find("#reset-password-alert"); + expect(alertSuccess.text()).toBe("Reset link has been sent to your email."); + }); + }); + + it("display error message", async () => { + const wrapper = mountResetPassword({ email: "test@test.com" }); + const submitButton = wrapper.find("#reset-password"); + + const mockAxios = new MockAdapter(axios); + mockAxios.onPost("/user/reset_password").reply(400, { + err_msg: "Please provide your email.", + }); + await submitButton.trigger("click"); + setTimeout(async () => { + const alertError = wrapper.find("#reset-password-alert"); + expect(alertError.text()).toBe("Please provide your email."); + }); }); }); diff --git a/client/src/entry/analysis/modules/ResetPassword.vue b/client/src/entry/analysis/modules/ResetPassword.vue index d56b07e6b7b2..716626ba2f46 100644 --- a/client/src/entry/analysis/modules/ResetPassword.vue +++ b/client/src/entry/analysis/modules/ResetPassword.vue @@ -35,16 +35,18 @@ async function resetLogin() {
- + {{ message }} - + - Reset your password + Send password reset email
From c02d41011f11b53ec3a8ad7f521287fda89be0c4 Mon Sep 17 00:00:00 2001 From: Jonathan Laperle Date: Fri, 5 Jul 2024 22:22:26 -0400 Subject: [PATCH 7/7] remove config mock --- .../analysis/modules/ResetPassword.test.ts | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/client/src/entry/analysis/modules/ResetPassword.test.ts b/client/src/entry/analysis/modules/ResetPassword.test.ts index cae83aa1b421..d172a1ecdce5 100644 --- a/client/src/entry/analysis/modules/ResetPassword.test.ts +++ b/client/src/entry/analysis/modules/ResetPassword.test.ts @@ -7,26 +7,6 @@ import ResetPassword from "./ResetPassword.vue"; const localVue = getLocalVue(true); -const configMock = { - allow_user_creation: true, - enable_oidc: true, - mailing_join_addr: "mailing_join_addr", - prefer_custos_login: true, - registration_warning_message: "registration_warning_message", - server_mail_configured: true, - show_welcome_with_login: true, - terms_url: "terms_url", - welcome_url: "welcome_url", -}; - -jest.mock("app/singleton"); -jest.mock("@/composables/config", () => ({ - useConfig: jest.fn(() => ({ - config: configMock, - isConfigLoaded: true, - })), -})); - const mockRouter = (query: object) => ({ currentRoute: { query,