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

feat: reset password feature #756

Merged
merged 8 commits into from
May 25, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/composables.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Customer } from '@shopware-pwa/commons/interfaces/models/checkout/custo
import { CustomerAddress } from '@shopware-pwa/commons/interfaces/models/checkout/customer/CustomerAddress';
import { CustomerAddressParam } from '@shopware-pwa/shopware-6-client';
import { CustomerRegistrationParams } from '@shopware-pwa/commons/interfaces/request/CustomerRegistrationParams';
import { CustomerResetPasswordParam } from '@shopware-pwa/shopware-6-client';
import { CustomerUpdateEmailParam } from '@shopware-pwa/shopware-6-client';
import { CustomerUpdatePasswordParam } from '@shopware-pwa/shopware-6-client';
import { CustomerUpdateProfileParam } from '@shopware-pwa/shopware-6-client';
Expand Down Expand Up @@ -300,6 +301,8 @@ export interface UseUser {
// (undocumented)
register: ({}: CustomerRegistrationParams) => Promise<boolean>;
// (undocumented)
resetPassword: (resetPasswordData: CustomerResetPasswordParam) => Promise<boolean>;
// (undocumented)
salutation: Ref<Salutation | null>;
// (undocumented)
updateEmail: (updateEmailData: CustomerUpdateEmailParam) => Promise<boolean>;
Expand Down
11 changes: 11 additions & 0 deletions api/shopware-6-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export interface CustomerRegisterResponse {
data: string;
}

// @alpha (undocumented)
export interface CustomerResetPasswordParam {
// (undocumented)
email: string;
// (undocumented)
storefrontUrl?: string;
}

// @alpha (undocumented)
export interface CustomerUpdateEmailParam {
// (undocumented)
Expand Down Expand Up @@ -253,6 +261,9 @@ export function register(params: CustomerRegistrationParams): Promise<CustomerRe
// @alpha
export function removeCartItem(itemId: string): Promise<Cart>;

// @alpha
export function resetPassword(params: CustomerResetPasswordParam): Promise<void>;

// @alpha
export function setCurrentBillingAddress(billingAddressId: string): Promise<ContextTokenResponse>;

Expand Down
25 changes: 25 additions & 0 deletions packages/composables/__tests__/useUser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,31 @@ describe("Composables - useUser", () => {
);
});
});
describe("resetPassword", () => {
it("should invoke resetPassword api-client method and return true on success", async () => {
mockedApiClient.resetPassword.mockImplementationOnce(async () =>
Promise.resolve(undefined)
);
Comment on lines +502 to +504
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is perfectly okay, more educational improvement here you can do
https://jestjs.io/docs/en/mock-function-api#mockfnmockreturnvalueoncevalue
and in case of mocking rejections like in line 513
https://jestjs.io/docs/en/mock-function-api#mockfnmockrejectedvalueoncevalue

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some good hints. Thank you :)

const { resetPassword } = useUser();
const response = await resetPassword({
email: "qweqwe@qwe.com",
});
expect(mockedApiClient.resetPassword).toBeCalledTimes(1);
expect(response).toBeTruthy();
});
it("should return false and set the error.value on api-client on resetPassword rejection", async () => {
mockedApiClient.resetPassword.mockImplementationOnce(async () =>
Promise.reject("Email does not fit to any in Sales Channel")
);
const { resetPassword, error } = useUser();
const response = await resetPassword({
email: "qweqwe@qwe.com",
});
expect(mockedApiClient.resetPassword).toBeCalledTimes(1);
expect(response).toBeFalsy();
expect(error.value).toEqual("Email does not fit to any in Sales Channel");
});
});
describe("updateEmail", () => {
it("should invoke updateEmail api-client method and return true on success", async () => {
mockedApiClient.updateEmail.mockImplementationOnce(async () =>
Expand Down
18 changes: 18 additions & 0 deletions packages/composables/src/hooks/useUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
logout as apiLogout,
register as apiRegister,
updatePassword as apiUpdatePassword,
resetPassword as apiResetPassword,
updateEmail as apiUpdateEmail,
getCustomer,
getCustomerOrders,
Expand All @@ -20,6 +21,7 @@ import {
CustomerUpdateProfileParam,
CustomerUpdatePasswordParam,
CustomerUpdateEmailParam,
CustomerResetPasswordParam,
} from "@shopware-pwa/shopware-6-client";
import { Customer } from "@shopware-pwa/commons/interfaces/models/checkout/customer/Customer";
import { getStore } from "@shopware-pwa/composables";
Expand Down Expand Up @@ -69,6 +71,9 @@ export interface UseUser {
updatePassword: (
updatePasswordData: CustomerUpdatePasswordParam
) => Promise<boolean>;
resetPassword: (
resetPasswordData: CustomerResetPasswordParam
) => Promise<boolean>;
markAddressAsDefault: ({
addressId,
type,
Expand Down Expand Up @@ -263,6 +268,18 @@ export const useUser = (): UseUser => {
return true;
};

const resetPassword = async (
resetPasswordData: CustomerResetPasswordParam
): Promise<boolean> => {
try {
await apiResetPassword(resetPasswordData);
} catch (e) {
error.value = e;
return false;
}
return true;
};

const updateEmail = async (
updateEmailData: CustomerUpdateEmailParam
): Promise<boolean> => {
Expand Down Expand Up @@ -295,6 +312,7 @@ export const useUser = (): UseUser => {
updateEmail,
updatePersonalInfo,
updatePassword,
resetPassword,
addAddress,
deleteAddress,
loadSalutation,
Expand Down
38 changes: 30 additions & 8 deletions packages/default-theme/components/SwResetPassword.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<template>
<div class="sw-reset-password" @keyup.enter="invokeResetPassword">
<div class="form sw-reset-password__form">
<div class="form sw-reset-password__form" v-if="!emailSent">
<!-- <h2 class="sw-reset-password__header">Reset password</h2> -->
<SfAlert
v-if="error"
v-if="userError"
class="sw-reset-password__alert"
type="danger"
:message="error"
:message="userError.message"
/>
<SfInput
v-model="email"
Expand All @@ -24,22 +24,37 @@
Resend password
</SfButton>
</div>
<SfHeading
v-if="emailSent"
title="You should receive a link in a few moments. Please open that link to reset your password."
:level="5"
class="bottom__heading"
/>
</div>
</template>

<script>
import { SfInput, SfButton, SfAlert } from '@storefront-ui/vue'
import { SfInput, SfButton, SfAlert, SfHeading } from '@storefront-ui/vue'
import { validationMixin } from 'vuelidate'
import { required, email } from 'vuelidate/lib/validators'
import {useUser} from '@shopware-pwa/composables';

export default {
name: 'SwResetPassword',
components: { SfButton, SfInput, SfAlert },
components: { SfButton, SfInput, SfAlert, SfHeading },
mixins: [validationMixin],
data() {
return {
email: '',
error: ''
error: '',
emailSent: false
}
},
setup() {
const { resetPassword, error: userError } = useUser()
return {
resetPassword: resetPassword,
userError,
}
},
validations: {
Expand All @@ -49,9 +64,16 @@ export default {
}
},
methods: {
invokeResetPassword() {
async invokeResetPassword() {
this.$v.$touch()
this.error = 'Reset password is not implemented yet'

if (this.$v.$invalid) {
return;
}

this.emailSent = await this.resetPassword({
email: this.email
})
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { getCustomerResetPasswordEndpoint } from "../../../src/endpoints";
import { apiService } from "../../../src/apiService";
import { internet, random } from "faker";
import { resetPassword, update, config } from "@shopware-pwa/shopware-6-client";

const DEFAULT_ENDPOINT = "https://shopware-2.vuestorefront.io";
const email = internet.email("John", "Doe");
const credentials = {
email: email,
storefrontUrl: config.endpoint ?? DEFAULT_ENDPOINT,
};

jest.mock("../../../src/apiService");
const mockedAxios = apiService as jest.Mocked<typeof apiService>;

describe("CustomerService - resetPassword", () => {
let contextToken: string;
beforeEach(() => {
jest.resetAllMocks();
contextToken = random.uuid();
update({ contextToken });
});
afterEach(() => {
expect(config.contextToken).toEqual(contextToken);
});

it("rejects the promise if the email do not mach any in Sales Channel", async () => {
mockedAxios.post.mockRejectedValueOnce(
new Error("400 - invalid email address")
);
expect(
resetPassword({
email: credentials.email,
storefrontUrl: credentials.storefrontUrl,
})
).rejects.toThrow("400 - invalid email");
expect(mockedAxios.post).toBeCalledTimes(1);
expect(mockedAxios.post).toBeCalledWith(
getCustomerResetPasswordEndpoint(),
{
email: credentials.email,
storefrontUrl: credentials.storefrontUrl,
}
);
});

it("returns no data if successfully updated", async () => {
mockedAxios.post.mockResolvedValueOnce(null);

const resultWithFullParams = await resetPassword({
email: credentials.email,
storefrontUrl: credentials.storefrontUrl,
});
expect(resultWithFullParams).toBeFalsy();

const resultWithEmptyUrl = await resetPassword({
email: credentials.email,
storefrontUrl: "",
});
expect(resultWithEmptyUrl).toBeFalsy();

const resultWithoutUrl = await resetPassword({
email: credentials.email,
});
expect(resultWithoutUrl).toBeFalsy();

expect(mockedAxios.post).toBeCalledTimes(3);
expect(mockedAxios.post).toBeCalledWith(
getCustomerResetPasswordEndpoint(),
{
email: credentials.email,
storefrontUrl: credentials.storefrontUrl,
}
);
});

it("should set storefrontUrl from config if not provided with params ", async () => {
await resetPassword({
email: credentials.email,
});
expect(mockedAxios.post).toBeCalledWith(
"/store-api/v1/account/recovery-password",
{
email: credentials.email,
storefrontUrl: "https://shopware6-demo.vuestorefront.io",
}
);
});

it("should invokde post method with null if params are not provided", async () => {
await resetPassword(null as any);
expect(mockedAxios.post).toBeCalledWith(
"/store-api/v1/account/recovery-password",
null
);
});
});
3 changes: 3 additions & 0 deletions packages/shopware-6-client/src/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export const getCustomerUpdateEmailEndpoint = () =>
export const getCustomerUpdatePasswordEndpoint = () =>
`/store-api/v1/account/change-password`;

export const getCustomerResetPasswordEndpoint = () =>
`/store-api/v1/account/recovery-password`;

// checkout

export const getCheckoutCartEndpoint = () =>
Expand Down
26 changes: 26 additions & 0 deletions packages/shopware-6-client/src/services/customerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getCustomerRegisterEndpoint,
getCustomerDetailsUpdateEndpoint,
getCustomerUpdatePasswordEndpoint,
getCustomerResetPasswordEndpoint,
getCustomerDefaultBillingAddressEndpoint,
getCustomerDefaultShippingAddressEndpoint,
getCustomerLogoutEndpoint,
Expand All @@ -13,6 +14,7 @@ import {
} from "../endpoints";
import { Customer } from "@shopware-pwa/commons/interfaces/models/checkout/customer/Customer";
import { apiService } from "../apiService";
import { config } from "../settings";
import { CustomerAddress } from "@shopware-pwa/commons/interfaces/models/checkout/customer/CustomerAddress";
import { CustomerRegistrationParams } from "@shopware-pwa/commons/interfaces/request/CustomerRegistrationParams";
import { ContextTokenResponse } from "@shopware-pwa/commons/interfaces/response/SessionContext";
Expand Down Expand Up @@ -246,6 +248,30 @@ export async function updatePassword(
await apiService.post(getCustomerUpdatePasswordEndpoint(), params);
}

/**
* @alpha
*/
export interface CustomerResetPasswordParam {
email: string;
storefrontUrl?: string;
}

/**
* Reset a customer's password
*
* @throws ClientApiError
* @alpha
*/
export async function resetPassword(
params: CustomerResetPasswordParam
): Promise<void> {
if (params && !params.storefrontUrl) {
params.storefrontUrl = config.endpoint;
}

await apiService.post(getCustomerResetPasswordEndpoint(), params);
}

/**
* @alpha
*/
Expand Down