Skip to content

Commit

Permalink
feat(core): Verify admin-created Customers if password supplied
Browse files Browse the repository at this point in the history
Relates to #171
  • Loading branch information
michaelbromley committed Sep 30, 2019
1 parent 70e857d commit 9931e25
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 18 deletions.
2 changes: 1 addition & 1 deletion packages/core/e2e/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ describe('Authorization & permissions', () => {
it('can create', async () => {
await assertRequestAllowed(
gql`
mutation CreateCustomer($input: CreateCustomerInput!) {
mutation CanCreateCustomer($input: CreateCustomerInput!) {
createCustomer(input: $input) {
id
}
Expand Down
90 changes: 85 additions & 5 deletions packages/core/e2e/customer.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { OnModuleInit } from '@nestjs/common';
import { omit } from '@vendure/common/lib/omit';
import gql from 'graphql-tag';
import path from 'path';

import { EventBus } from '../src/event-bus/event-bus';
import { EventBusModule } from '../src/event-bus/event-bus.module';
import { AccountRegistrationEvent } from '../src/event-bus/events/account-registration-event';
import { VendurePlugin } from '../src/plugin/vendure-plugin';

import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
import { CUSTOMER_FRAGMENT } from './graphql/fragments';
import {
CreateAddress,
CreateCustomer,
DeleteCustomer,
DeleteCustomerAddress,
DeletionResult,
Expand All @@ -23,6 +30,7 @@ import { TestServer } from './test-server';
import { assertThrowsWithMessage } from './utils/assert-throws-with-message';

// tslint:disable:no-non-null-assertion
let sendEmailFn: jest.Mock;

describe('Customer resolver', () => {
const adminClient = new TestAdminClient();
Expand All @@ -33,10 +41,15 @@ describe('Customer resolver', () => {
let thirdCustomer: GetCustomerList.Items;

beforeAll(async () => {
const token = await server.init({
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
customerCount: 5,
});
const token = await server.init(
{
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
customerCount: 5,
},
{
plugins: [TestEmailPlugin],
},
);
await adminClient.init();
}, TEST_SETUP_TIMEOUT_MS);

Expand Down Expand Up @@ -303,9 +316,51 @@ describe('Customer resolver', () => {
});
});

describe('creation', () => {
it('triggers verification event if no password supplied', async () => {
sendEmailFn = jest.fn();
const { createCustomer } = await adminClient.query<
CreateCustomer.Mutation,
CreateCustomer.Variables
>(CREATE_CUSTOMER, {
input: {
emailAddress: '[email protected]',
firstName: 'New',
lastName: 'Customer',
},
});

expect(createCustomer.user!.verified).toBe(false);
expect(sendEmailFn).toHaveBeenCalledTimes(1);
expect(sendEmailFn.mock.calls[0][0] instanceof AccountRegistrationEvent).toBe(true);
expect(sendEmailFn.mock.calls[0][0].user.identifier).toBe('[email protected]');
});

it('creates a verified Customer', async () => {
sendEmailFn = jest.fn();
const { createCustomer } = await adminClient.query<
CreateCustomer.Mutation,
CreateCustomer.Variables
>(CREATE_CUSTOMER, {
input: {
emailAddress: '[email protected]',
firstName: 'New',
lastName: 'Customer',
},
password: 'test',
});

expect(createCustomer.user!.verified).toBe(true);
expect(sendEmailFn).toHaveBeenCalledTimes(0);
});
});

describe('deletion', () => {
it('deletes a customer', async () => {
const result = await adminClient.query<DeleteCustomer.Mutation, DeleteCustomer.Variables>(DELETE_CUSTOMER, { id: thirdCustomer.id });
const result = await adminClient.query<DeleteCustomer.Mutation, DeleteCustomer.Variables>(
DELETE_CUSTOMER,
{ id: thirdCustomer.id },
);

expect(result.deleteCustomer).toEqual({ result: DeletionResult.DELETED });
});
Expand Down Expand Up @@ -406,6 +461,15 @@ const GET_CUSTOMER_ORDERS = gql`
}
`;

export const CREATE_CUSTOMER = gql`
mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
createCustomer(input: $input, password: $password) {
...Customer
}
}
${CUSTOMER_FRAGMENT}
`;

export const UPDATE_CUSTOMER = gql`
mutation UpdateCustomer($input: UpdateCustomerInput!) {
updateCustomer(input: $input) {
Expand All @@ -422,3 +486,19 @@ const DELETE_CUSTOMER = gql`
}
}
`;

/**
* This mock plugin simulates an EmailPlugin which would send emails
* on the registration & password reset events.
*/
@VendurePlugin({
imports: [EventBusModule],
})
class TestEmailPlugin implements OnModuleInit {
constructor(private eventBus: EventBus) {}
onModuleInit() {
this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
sendEmailFn(event);
});
}
}
27 changes: 21 additions & 6 deletions packages/core/e2e/graphql/generated-e2e-admin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3350,11 +3350,11 @@ export type UpdateAdministratorMutation = { __typename?: 'Mutation' } & {
updateAdministrator: { __typename?: 'Administrator' } & AdministratorFragment;
};

export type CreateCustomerMutationVariables = {
export type CanCreateCustomerMutationVariables = {
input: CreateCustomerInput;
};

export type CreateCustomerMutation = { __typename?: 'Mutation' } & {
export type CanCreateCustomerMutation = { __typename?: 'Mutation' } & {
createCustomer: { __typename?: 'Customer' } & Pick<Customer, 'id'>;
};

Expand Down Expand Up @@ -3595,6 +3595,15 @@ export type GetCustomerOrdersQuery = { __typename?: 'Query' } & {
>;
};

export type CreateCustomerMutationVariables = {
input: CreateCustomerInput;
password?: Maybe<Scalars['String']>;
};

export type CreateCustomerMutation = { __typename?: 'Mutation' } & {
createCustomer: { __typename?: 'Customer' } & CustomerFragment;
};

export type UpdateCustomerMutationVariables = {
input: UpdateCustomerInput;
};
Expand Down Expand Up @@ -4962,10 +4971,10 @@ export namespace UpdateAdministrator {
export type UpdateAdministrator = AdministratorFragment;
}

export namespace CreateCustomer {
export type Variables = CreateCustomerMutationVariables;
export type Mutation = CreateCustomerMutation;
export type CreateCustomer = CreateCustomerMutation['createCustomer'];
export namespace CanCreateCustomer {
export type Variables = CanCreateCustomerMutationVariables;
export type Mutation = CanCreateCustomerMutation;
export type CreateCustomer = CanCreateCustomerMutation['createCustomer'];
}

export namespace GetCustomerCount {
Expand Down Expand Up @@ -5136,6 +5145,12 @@ export namespace GetCustomerOrders {
export type Items = NonNullable<(NonNullable<GetCustomerOrdersQuery['customer']>)['orders']['items'][0]>;
}

export namespace CreateCustomer {
export type Variables = CreateCustomerMutationVariables;
export type Mutation = CreateCustomerMutation;
export type CreateCustomer = CustomerFragment;
}

export namespace UpdateCustomer {
export type Variables = UpdateCustomerMutationVariables;
export type Mutation = UpdateCustomerMutation;
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/api/resolvers/admin/customer.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ export class CustomerResolver {

@Mutation()
@Allow(Permission.CreateCustomer)
async createCustomer(@Args() args: MutationCreateCustomerArgs): Promise<Customer> {
async createCustomer(
@Ctx() ctx: RequestContext,
@Args() args: MutationCreateCustomerArgs,
): Promise<Customer> {
const { input, password } = args;
return this.customerService.create(input, password || undefined);
return this.customerService.create(ctx, input, password || undefined);
}

@Mutation()
Expand Down
20 changes: 16 additions & 4 deletions packages/core/src/service/services/customer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class CustomerService {
});
}

async create(input: CreateCustomerInput, password?: string): Promise<Customer> {
async create(ctx: RequestContext, input: CreateCustomerInput, password?: string): Promise<Customer> {
input.emailAddress = normalizeEmailAddress(input.emailAddress);
const customer = new Customer(input);

Expand All @@ -93,9 +93,17 @@ export class CustomerService {
if (existing) {
throw new InternalServerError(`error.email-address-must-be-unique`);
}
customer.user = await this.userService.createCustomerUser(input.emailAddress, password);

if (password) {
customer.user = await this.userService.createCustomerUser(input.emailAddress, password);
if (password && password !== '') {
if (customer.user.verificationToken) {
customer.user = await this.userService.verifyUserByToken(
customer.user.verificationToken,
password,
);
}
} else {
this.eventBus.publish(new AccountRegistrationEvent(ctx, customer.user));
}
return this.connection.getRepository(Customer).save(customer);
}
Expand Down Expand Up @@ -168,7 +176,11 @@ export class CustomerService {
}
}

async requestUpdateEmailAddress(ctx: RequestContext, userId: ID, newEmailAddress: string): Promise<boolean> {
async requestUpdateEmailAddress(
ctx: RequestContext,
userId: ID,
newEmailAddress: string,
): Promise<boolean> {
const userWithConflictingIdentifier = await this.userService.getUserByEmailAddress(newEmailAddress);
if (userWithConflictingIdentifier) {
throw new UserInputError('error.email-address-not-available');
Expand Down

0 comments on commit 9931e25

Please sign in to comment.