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 1 commit
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 @@ -90,6 +90,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 @@ -251,6 +259,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
2 changes: 1 addition & 1 deletion packages/composables/__tests__/useProductListing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ describe("Composables - useProductListing", () => {

it("should not change pagination state to privided one once a useProductListing argument is passed hasn't any required fields", async () => {
const { pagination } = useProductListing({
page: undefined
page: undefined,
} as any);

expect(pagination.value).toStrictEqual({
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 @@ -264,6 +269,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 @@ -296,6 +313,7 @@ export const useUser = (): UseUser => {
updateEmail,
updatePersonalInfo,
updatePassword,
resetPassword,
addAddress,
deleteAddress,
loadSalutation,
Expand Down
25 changes: 21 additions & 4 deletions packages/default-theme/components/SwResetPassword.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<div class="form sw-reset-password__form">
<!-- <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 @@ -31,6 +31,7 @@
import { SfInput, SfButton, SfAlert } 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',
Expand All @@ -42,16 +43,32 @@ export default {
error: ''
}
},
setup() {
const { resetPassword, error: userError } = useUser()
return {
resetPassword: resetPassword,
userError,
}
},
validations: {
email: {
required,
email
}
},
methods: {
invokeResetPassword() {
async invokeResetPassword() {
this.$v.$touch()
this.error = 'Reset password is not implemented yet'

if (this.$v.$invalid) {
return;
}
const resetSent = await this.resetPassword({
email: this.email
})
if (resetSent) {
this.$emit('success');
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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 result = await resetPassword({
email: credentials.email,
storefrontUrl: credentials.storefrontUrl ?? "",
});
expect(result).toBeFalsy();
expect(mockedAxios.post).toBeCalledTimes(1);
expect(mockedAxios.post).toBeCalledWith(
getCustomerResetPasswordEndpoint(),
{
email: credentials.email,
storefrontUrl: credentials.storefrontUrl ?? "",
}
);
});
});
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 @@ -14,6 +15,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 @@ -238,6 +240,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.storefrontUrl) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

in case of params being null we should prevent error.

Suggested change
if (!params.storefrontUrl) {
if (!params?.storefrontUrl) {

more about: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining
This will require to add unit test invoking method with no params like

const result = await resetPassword(null as any);

Copy link
Collaborator Author

@mmularski mmularski May 21, 2020

Choose a reason for hiding this comment

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

  1. I have added optional chaining here but it makes a problem with branch coverage due to
    Optional chaining does not count as branch for coverage istanbuljs/istanbuljs#516

Just added a workaround with /* istanbul ignore next */ FYI :)

This will require to add unit test invoking method with no params like
const result = await resetPassword(null as any);

I do not get this. null as any will cause TypeError due to inconsistency with CustomerResetPasswordParam or I just do not understand something? :D

  1. After a successful operation, modal just closes and email is sending. I was modeling on login operation.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Here's what I meant with this test, turned out it has to be another if statement. What we needed to test:

  1. invocation with no parameter provided
  2. invocation without any parameters provided

please take a look at this commit: 29bc328

Why should we test for 2? Because library compiles to JS and someone might unintentionally ignore param for method. We need to be resistant to this kind of behaviour:)

params.storefrontUrl = config.endpoint;
}

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

/**
* @alpha
*/
Expand Down