From 0a57168093d0ca29bb36f92592c0d0f25224f0e6 Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Tue, 19 Nov 2019 11:06:45 -0800 Subject: [PATCH 1/4] feat: add addAccountEmailRecord mutation and corresponding integration test Signed-off-by: Will Lopez --- .../mutations/addAccountEmailRecord.js | 85 +++++++++++++++++++ src/core-services/account/mutations/index.js | 2 + .../Mutation/addAccountEmailRecord.js | 29 +++++++ .../account/resolvers/Mutation/index.js | 2 + src/core-services/account/simpleSchemas.js | 2 +- .../AddAccountEmailRecordMutation.graphql | 9 ++ .../addAccountEmailRecord.test.js | 58 +++++++++++++ tests/util/factory.js | 2 + 8 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/core-services/account/mutations/addAccountEmailRecord.js create mode 100644 src/core-services/account/resolvers/Mutation/addAccountEmailRecord.js create mode 100644 tests/integration/api/mutations/addAccountEmailRecord/AddAccountEmailRecordMutation.graphql create mode 100644 tests/integration/api/mutations/addAccountEmailRecord/addAccountEmailRecord.test.js diff --git a/src/core-services/account/mutations/addAccountEmailRecord.js b/src/core-services/account/mutations/addAccountEmailRecord.js new file mode 100644 index 00000000000..19fe7bbc41b --- /dev/null +++ b/src/core-services/account/mutations/addAccountEmailRecord.js @@ -0,0 +1,85 @@ +import SimpleSchema from "simpl-schema"; +import ReactionError from "@reactioncommerce/reaction-error"; +import sendVerificationEmail from "../util/sendVerificationEmail.js"; + +const inputSchema = new SimpleSchema({ + accountId: String, + email: SimpleSchema.RegEx.Email +}); + +/** + * @name accounts/addAccountEmailRecord + * @memberof Mutations/Accounts + * @summary adds a new email to the user's profile + * @param {Object} context - GraphQL execution context + * @param {Object} input - Necessary input for mutation. See SimpleSchema. + * @param {String} input.accountId - decoded ID of account on which entry should be added + * @param {String} input.email - the email to add to the account + * @returns {Promise} account with addd email + */ +export default async function addAccountEmailRecord(context, input) { + inputSchema.validate(input); + const { appEvents, checkPermissions, collections, userId: userIdFromContext } = context; + const { Accounts, users } = collections; + const { + accountId, + email + } = input; + + const account = await Accounts.findOne({ _id: accountId }); + if (!account) throw new ReactionError("not-found", "Account not Found"); + + const user = await users.findOne({ _id: account.userId }); + if (!user) throw new ReactionError("not-found", "User not Found"); + + if (!context.isInternalCall && userIdFromContext !== account.userId) { + await checkPermissions(["reaction-accounts"], account.shopId); + } + + // add email to user + const { value: updatedUser } = await users.findOneAndUpdate( + { _id: user._id }, + { + $addToSet: { + emails: { + address: email, + provides: "default", + verified: false + } + } + }, + { + returnOriginal: false + } + ); + + if (!updatedUser) throw new ReactionError("server-error", "Unable to update User"); + + // add email to Account + const { value: updatedAccount } = await Accounts.findOneAndUpdate( + { _id: accountId }, + { + $set: { + emails: updatedUser.emails + } + }, + { + returnOriginal: false + } + ); + + if (!updatedAccount) throw new ReactionError("server-error", "Unable to update Account"); + + sendVerificationEmail(context, { + bodyTemplate: "accounts/verifyUpdatedEmail", + userId: user._id + }); + + await appEvents.emit("afterAccountUpdate", { + account: updatedAccount, + updatedBy: accountId, + updatedFields: ["emails"] + }); + + return updatedAccount; +} diff --git a/src/core-services/account/mutations/index.js b/src/core-services/account/mutations/index.js index d62a622607e..bd3fbcbafe3 100644 --- a/src/core-services/account/mutations/index.js +++ b/src/core-services/account/mutations/index.js @@ -1,4 +1,5 @@ import addressBookAdd from "./addressBookAdd.js"; +import addAccountEmailRecord from "./addAccountEmailRecord.js"; import addAccountToGroup from "./addAccountToGroup.js"; import addAccountToGroupBySlug from "./addAccountToGroupBySlug.js"; import createAccount from "./createAccount.js"; @@ -13,6 +14,7 @@ import updateAccountAddressBookEntry from "./updateAccountAddressBookEntry.js"; export default { addressBookAdd, + addAccountEmailRecord, addAccountToGroup, addAccountToGroupBySlug, createAccount, diff --git a/src/core-services/account/resolvers/Mutation/addAccountEmailRecord.js b/src/core-services/account/resolvers/Mutation/addAccountEmailRecord.js new file mode 100644 index 00000000000..e644a9b63cc --- /dev/null +++ b/src/core-services/account/resolvers/Mutation/addAccountEmailRecord.js @@ -0,0 +1,29 @@ +import { decodeAccountOpaqueId } from "../../xforms/id.js"; + +/** + * @name Mutation/addAccountEmailRecord + * @method + * @memberof Accounts/GraphQL + * @summary resolver for the addAccountEmailRecord GraphQL mutation + * @param {Object} _ - unused + * @param {Object} args.input - an object of all mutation arguments that were sent by the client + * @param {String} args.input.accountId - The account ID + * @param {String} args.input.email - The email to add + * @param {String} [args.input.clientMutationId] - An optional string identifying the mutation call + * @param {Object} context - an object containing the per-request state + * @returns {Object} addAccountEmailRecordPayload + */ +export default async function addAccountEmailRecord(_, { input }, context) { + const { accountId, email, clientMutationId = null } = input; + const decodedAccountId = decodeAccountOpaqueId(accountId); + + const updatedAccount = await context.mutations.addAccountEmailRecord(context, { + accountId: decodedAccountId, + email + }); + + return { + emailRecord: updatedAccount.emails.pop(), + clientMutationId + }; +} diff --git a/src/core-services/account/resolvers/Mutation/index.js b/src/core-services/account/resolvers/Mutation/index.js index 2dee4cc07cb..6d57eee85e4 100644 --- a/src/core-services/account/resolvers/Mutation/index.js +++ b/src/core-services/account/resolvers/Mutation/index.js @@ -1,4 +1,5 @@ import addAccountAddressBookEntry from "./addAccountAddressBookEntry.js"; +import addAccountEmailRecord from "./addAccountEmailRecord.js"; import addAccountToGroup from "./addAccountToGroup.js"; import inviteShopMember from "./inviteShopMember.js"; import removeAccountAddressBookEntry from "./removeAccountAddressBookEntry.js"; @@ -10,6 +11,7 @@ import updateAccountAddressBookEntry from "./updateAccountAddressBookEntry.js"; export default { addAccountAddressBookEntry, + addAccountEmailRecord, addAccountToGroup, inviteShopMember, removeAccountAddressBookEntry, diff --git a/src/core-services/account/simpleSchemas.js b/src/core-services/account/simpleSchemas.js index 5f45f3d753f..9b9f791890b 100644 --- a/src/core-services/account/simpleSchemas.js +++ b/src/core-services/account/simpleSchemas.js @@ -250,7 +250,7 @@ export const Profile = new SimpleSchema({ * @property {String} address required * @property {Boolean} verified optional */ -const Email = new SimpleSchema({ +export const Email = new SimpleSchema({ provides: { type: String, defaultValue: "default", diff --git a/tests/integration/api/mutations/addAccountEmailRecord/AddAccountEmailRecordMutation.graphql b/tests/integration/api/mutations/addAccountEmailRecord/AddAccountEmailRecordMutation.graphql new file mode 100644 index 00000000000..2b1ea2c5648 --- /dev/null +++ b/tests/integration/api/mutations/addAccountEmailRecord/AddAccountEmailRecordMutation.graphql @@ -0,0 +1,9 @@ +mutation ($accountId: ID!, $email: Email!) { + addAccountEmailRecord(input: { accountId: $accountId, email: $email }) { + emailRecord { + address + provides + verified + } + } +} diff --git a/tests/integration/api/mutations/addAccountEmailRecord/addAccountEmailRecord.test.js b/tests/integration/api/mutations/addAccountEmailRecord/addAccountEmailRecord.test.js new file mode 100644 index 00000000000..b0f81cb5e5d --- /dev/null +++ b/tests/integration/api/mutations/addAccountEmailRecord/addAccountEmailRecord.test.js @@ -0,0 +1,58 @@ +import importAsString from "@reactioncommerce/api-utils/importAsString.js"; +import encodeOpaqueId from "@reactioncommerce/api-utils/encodeOpaqueId.js"; +import Factory from "/tests/util/factory.js"; +import TestApp from "/tests/util/TestApp.js"; + +const AddAccountEmailRecordMutation = importAsString("./AddAccountEmailRecordMutation.graphql"); + +jest.setTimeout(300000); + +let testApp; +let addAccountEmailRecord; +let shopId; +let mockUserAccount; +let accountOpaqueId; + +beforeAll(async () => { + testApp = new TestApp(); + await testApp.start(); + shopId = await testApp.insertPrimaryShop(); + addAccountEmailRecord = testApp.mutate(AddAccountEmailRecordMutation); + + mockUserAccount = Factory.Account.makeOne({ + _id: "mockUserId", + groups: [], + roles: {}, + shopId + }); + + accountOpaqueId = encodeOpaqueId("reaction/account", mockUserAccount._id); + + await testApp.createUserAndAccount(mockUserAccount); +}); + +afterAll(async () => { + await testApp.collections.Accounts.deleteMany({}); + await testApp.collections.users.deleteMany({}); + await testApp.collections.Shops.deleteMany({}); + await testApp.stop(); +}); + +test("user can add an email to their own account", async () => { + await testApp.setLoggedInUser(mockUserAccount); + + const email = Factory.Email.makeOne(); + + // _id is set by the server, true + delete email._id; + + let result; + try { + result = await addAccountEmailRecord({ accountId: accountOpaqueId, email: email.address }); + } catch (error) { + expect(error).toBeUndefined(); + return; + } + + expect(result.addAccountEmailRecord.emailRecord).toEqual(email); +}); diff --git a/tests/util/factory.js b/tests/util/factory.js index 59ab0ec9ab7..cfe40f416b8 100644 --- a/tests/util/factory.js +++ b/tests/util/factory.js @@ -41,6 +41,7 @@ import { import { Account, AccountProfileAddress, + Email, Group } from "../../src/core-services/account/simpleSchemas.js"; @@ -83,6 +84,7 @@ const schemasToAddToFactory = { CommonOrder, CommonOrderItem, Discounts, + Email, Group, Order, OrderAddress, From ad6f72564bdeb99c7000880db09685696629edd3 Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Tue, 19 Nov 2019 16:38:08 -0800 Subject: [PATCH 2/4] refactor: return full account object Signed-off-by: Will Lopez --- .../account/resolvers/Mutation/addAccountEmailRecord.js | 2 +- src/core-services/account/schemas/account.graphql | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core-services/account/resolvers/Mutation/addAccountEmailRecord.js b/src/core-services/account/resolvers/Mutation/addAccountEmailRecord.js index e644a9b63cc..a5bd696aa7d 100644 --- a/src/core-services/account/resolvers/Mutation/addAccountEmailRecord.js +++ b/src/core-services/account/resolvers/Mutation/addAccountEmailRecord.js @@ -23,7 +23,7 @@ export default async function addAccountEmailRecord(_, { input }, context) { }); return { - emailRecord: updatedAccount.emails.pop(), + account: updatedAccount, clientMutationId }; } diff --git a/src/core-services/account/schemas/account.graphql b/src/core-services/account/schemas/account.graphql index f10b79fd4a6..4f16f768b77 100644 --- a/src/core-services/account/schemas/account.graphql +++ b/src/core-services/account/schemas/account.graphql @@ -282,8 +282,8 @@ type AddAccountEmailRecordPayload { "The same string you sent with the mutation params, for matching mutation calls with their responses" clientMutationId: String - "The added email record" - emailRecord: EmailRecord + "The account, with updated `emailRecords`" + account: Account } "The response from the `createAccount` mutation" From 31021f13ff0f90065ee424a9be0ae82840429ac2 Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Tue, 19 Nov 2019 16:44:34 -0800 Subject: [PATCH 3/4] chore: update test to match updated return payload Signed-off-by: Will Lopez --- .../AddAccountEmailRecordMutation.graphql | 10 ++++++---- .../addAccountEmailRecord.test.js | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/integration/api/mutations/addAccountEmailRecord/AddAccountEmailRecordMutation.graphql b/tests/integration/api/mutations/addAccountEmailRecord/AddAccountEmailRecordMutation.graphql index 2b1ea2c5648..8b7cf5a3ea9 100644 --- a/tests/integration/api/mutations/addAccountEmailRecord/AddAccountEmailRecordMutation.graphql +++ b/tests/integration/api/mutations/addAccountEmailRecord/AddAccountEmailRecordMutation.graphql @@ -1,9 +1,11 @@ mutation ($accountId: ID!, $email: Email!) { addAccountEmailRecord(input: { accountId: $accountId, email: $email }) { - emailRecord { - address - provides - verified + account { + emailRecords { + address + provides + verified + } } } } diff --git a/tests/integration/api/mutations/addAccountEmailRecord/addAccountEmailRecord.test.js b/tests/integration/api/mutations/addAccountEmailRecord/addAccountEmailRecord.test.js index b0f81cb5e5d..77430958752 100644 --- a/tests/integration/api/mutations/addAccountEmailRecord/addAccountEmailRecord.test.js +++ b/tests/integration/api/mutations/addAccountEmailRecord/addAccountEmailRecord.test.js @@ -54,5 +54,5 @@ test("user can add an email to their own account", async () => { return; } - expect(result.addAccountEmailRecord.emailRecord).toEqual(email); + expect(result.addAccountEmailRecord.account.emailRecords.pop()).toEqual(email); }); From 0a6fb1ff1309a527b477695add9b54f2acf762ef Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Tue, 19 Nov 2019 16:49:39 -0800 Subject: [PATCH 4/4] fix: gql lint error Signed-off-by: Will Lopez --- src/core-services/account/schemas/account.graphql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core-services/account/schemas/account.graphql b/src/core-services/account/schemas/account.graphql index 4f16f768b77..c82fe6dc87c 100644 --- a/src/core-services/account/schemas/account.graphql +++ b/src/core-services/account/schemas/account.graphql @@ -279,11 +279,11 @@ type AddAccountAddressBookEntryPayload { "The response from the `addAccountEmailRecord` mutation" type AddAccountEmailRecordPayload { - "The same string you sent with the mutation params, for matching mutation calls with their responses" - clientMutationId: String - "The account, with updated `emailRecords`" account: Account + + "The same string you sent with the mutation params, for matching mutation calls with their responses" + clientMutationId: String } "The response from the `createAccount` mutation"