diff --git a/packages/core/e2e/auth.e2e-spec.ts b/packages/core/e2e/auth.e2e-spec.ts index 7f2c5d919f..a28746432a 100644 --- a/packages/core/e2e/auth.e2e-spec.ts +++ b/packages/core/e2e/auth.e2e-spec.ts @@ -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 } diff --git a/packages/core/e2e/customer.e2e-spec.ts b/packages/core/e2e/customer.e2e-spec.ts index babb57a097..51b5520d24 100644 --- a/packages/core/e2e/customer.e2e-spec.ts +++ b/packages/core/e2e/customer.e2e-spec.ts @@ -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, @@ -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(); @@ -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); @@ -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: 'test1@test.com', + 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('test1@test.com'); + }); + + it('creates a verified Customer', async () => { + sendEmailFn = jest.fn(); + const { createCustomer } = await adminClient.query< + CreateCustomer.Mutation, + CreateCustomer.Variables + >(CREATE_CUSTOMER, { + input: { + emailAddress: 'test2@test.com', + 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(DELETE_CUSTOMER, { id: thirdCustomer.id }); + const result = await adminClient.query( + DELETE_CUSTOMER, + { id: thirdCustomer.id }, + ); expect(result.deleteCustomer).toEqual({ result: DeletionResult.DELETED }); }); @@ -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) { @@ -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); + }); + } +} diff --git a/packages/core/e2e/graphql/generated-e2e-admin-types.ts b/packages/core/e2e/graphql/generated-e2e-admin-types.ts index 0e24a049b4..dc995e2698 100644 --- a/packages/core/e2e/graphql/generated-e2e-admin-types.ts +++ b/packages/core/e2e/graphql/generated-e2e-admin-types.ts @@ -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; }; @@ -3595,6 +3595,15 @@ export type GetCustomerOrdersQuery = { __typename?: 'Query' } & { >; }; +export type CreateCustomerMutationVariables = { + input: CreateCustomerInput; + password?: Maybe; +}; + +export type CreateCustomerMutation = { __typename?: 'Mutation' } & { + createCustomer: { __typename?: 'Customer' } & CustomerFragment; +}; + export type UpdateCustomerMutationVariables = { input: UpdateCustomerInput; }; @@ -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 { @@ -5136,6 +5145,12 @@ export namespace GetCustomerOrders { export type Items = NonNullable<(NonNullable)['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; diff --git a/packages/core/src/api/resolvers/admin/customer.resolver.ts b/packages/core/src/api/resolvers/admin/customer.resolver.ts index cfcab7cabd..2fd0e896e5 100644 --- a/packages/core/src/api/resolvers/admin/customer.resolver.ts +++ b/packages/core/src/api/resolvers/admin/customer.resolver.ts @@ -39,9 +39,12 @@ export class CustomerResolver { @Mutation() @Allow(Permission.CreateCustomer) - async createCustomer(@Args() args: MutationCreateCustomerArgs): Promise { + async createCustomer( + @Ctx() ctx: RequestContext, + @Args() args: MutationCreateCustomerArgs, + ): Promise { const { input, password } = args; - return this.customerService.create(input, password || undefined); + return this.customerService.create(ctx, input, password || undefined); } @Mutation() diff --git a/packages/core/src/service/services/customer.service.ts b/packages/core/src/service/services/customer.service.ts index 4bf8a27224..cbaaba63f3 100644 --- a/packages/core/src/service/services/customer.service.ts +++ b/packages/core/src/service/services/customer.service.ts @@ -80,7 +80,7 @@ export class CustomerService { }); } - async create(input: CreateCustomerInput, password?: string): Promise { + async create(ctx: RequestContext, input: CreateCustomerInput, password?: string): Promise { input.emailAddress = normalizeEmailAddress(input.emailAddress); const customer = new Customer(input); @@ -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); } @@ -168,7 +176,11 @@ export class CustomerService { } } - async requestUpdateEmailAddress(ctx: RequestContext, userId: ID, newEmailAddress: string): Promise { + async requestUpdateEmailAddress( + ctx: RequestContext, + userId: ID, + newEmailAddress: string, + ): Promise { const userWithConflictingIdentifier = await this.userService.getUserByEmailAddress(newEmailAddress); if (userWithConflictingIdentifier) { throw new UserInputError('error.email-address-not-available');