@labdigital/commercetools-mock 2.23.1 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labdigital/commercetools-mock",
3
- "version": "2.23.1",
3
+ "version": "2.26.0",
4
4
  "license": "MIT",
5
5
  "author": "Michael van Tellingen",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import { Customer } from "@commercetools/platform-sdk";
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
 
4
4
  const PWRESET_SECRET = "pwreset";
5
+ const EMAIL_VERIFY_SECRET = "emailverifysecret";
5
6
 
6
7
  export const validatePassword = (
7
8
  clearPassword: string,
@@ -16,6 +17,11 @@ export const createPasswordResetToken = (customer: Customer) =>
16
17
  "base64",
17
18
  );
18
19
 
20
+ export const createEmailVerifyToken = (customer: Customer) =>
21
+ Buffer.from(`${customer.id}:${EMAIL_VERIFY_SECRET}:${uuidv4()}`).toString(
22
+ "base64",
23
+ );
24
+
19
25
  export const validatePasswordResetToken = (token: string) => {
20
26
  const items = Buffer.from(token, "base64").toString("utf-8").split(":");
21
27
  const [customerId, secret] = items;
@@ -25,3 +31,12 @@ export const validatePasswordResetToken = (token: string) => {
25
31
 
26
32
  return customerId;
27
33
  };
34
+ export const validateEmailVerifyToken = (token: string) => {
35
+ const items = Buffer.from(token, "base64").toString("utf-8").split(":");
36
+ const [customerId, secret] = items;
37
+ if (secret !== EMAIL_VERIFY_SECRET) {
38
+ return undefined;
39
+ }
40
+
41
+ return customerId;
42
+ };
@@ -6,9 +6,11 @@ import type {
6
6
  CustomerSetCompanyNameAction,
7
7
  CustomerSetCustomFieldAction,
8
8
  CustomerSetCustomerNumberAction,
9
+ CustomerSetExternalIdAction,
9
10
  CustomerSetFirstNameAction,
10
11
  CustomerSetKeyAction,
11
12
  CustomerSetLastNameAction,
13
+ CustomerSetLocaleAction,
12
14
  CustomerSetSalutationAction,
13
15
  CustomerSetVatIdAction,
14
16
  CustomerUpdateAction,
@@ -141,6 +143,14 @@ export class CustomerUpdateHandler
141
143
  resource.custom.fields[name] = value;
142
144
  }
143
145
 
146
+ setExternalId(
147
+ _context: RepositoryContext,
148
+ resource: Writable<Customer>,
149
+ { externalId }: CustomerSetExternalIdAction,
150
+ ) {
151
+ resource.externalId = externalId;
152
+ }
153
+
144
154
  setFirstName(
145
155
  _context: RepositoryContext,
146
156
  resource: Writable<Customer>,
@@ -165,6 +175,14 @@ export class CustomerUpdateHandler
165
175
  resource.lastName = lastName;
166
176
  }
167
177
 
178
+ setLocale(
179
+ _context: RepositoryContext,
180
+ resource: Writable<Customer>,
181
+ { locale }: CustomerSetLocaleAction,
182
+ ) {
183
+ resource.locale = locale;
184
+ }
185
+
168
186
  setSalutation(
169
187
  _context: RepositoryContext,
170
188
  resource: Writable<Customer>,
@@ -7,7 +7,11 @@ import type {
7
7
  } from "@commercetools/platform-sdk";
8
8
  import { CommercetoolsError } from "~src/exceptions";
9
9
  import { generateRandomString, getBaseResourceProperties } from "~src/helpers";
10
- import { createPasswordResetToken, hashPassword } from "~src/lib/password";
10
+ import {
11
+ createEmailVerifyToken,
12
+ createPasswordResetToken,
13
+ hashPassword,
14
+ } from "~src/lib/password";
11
15
  import { AbstractStorage } from "~src/storage/abstract";
12
16
  import {
13
17
  AbstractResourceRepository,
@@ -110,4 +114,29 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
110
114
  value: token,
111
115
  };
112
116
  }
117
+
118
+ verifyEmailToken(context: RepositoryContext, id: string): CustomerToken {
119
+ const results = this._storage.query(context.projectKey, this.getTypeId(), {
120
+ where: [`id="${id.toLocaleLowerCase()}"`],
121
+ });
122
+ if (results.count === 0) {
123
+ throw new CommercetoolsError<ResourceNotFoundError>({
124
+ code: "ResourceNotFound",
125
+ message: `The Customer with ID '${id}' was not found.`,
126
+ });
127
+ }
128
+ const expiresAt = new Date(Date.now() + 30 * 60);
129
+ const customer = results.results[0] as Customer;
130
+ const rest = getBaseResourceProperties();
131
+
132
+ const token = createEmailVerifyToken(customer);
133
+ return {
134
+ id: rest.id,
135
+ createdAt: rest.createdAt,
136
+ lastModifiedAt: rest.lastModifiedAt,
137
+ customerId: customer.id,
138
+ expiresAt: expiresAt.toISOString(),
139
+ value: token,
140
+ };
141
+ }
113
142
  }
@@ -2,11 +2,16 @@ import {
2
2
  Customer,
3
3
  InvalidCurrentPasswordError,
4
4
  MyCustomerChangePassword,
5
+ MyCustomerEmailVerify,
5
6
  ResourceNotFoundError,
6
7
  type MyCustomerResetPassword,
7
8
  } from "@commercetools/platform-sdk";
8
9
  import { CommercetoolsError } from "~src/exceptions";
9
- import { hashPassword, validatePasswordResetToken } from "../lib/password";
10
+ import {
11
+ hashPassword,
12
+ validateEmailVerifyToken,
13
+ validatePasswordResetToken,
14
+ } from "../lib/password";
10
15
  import { Writable } from "../types";
11
16
  import { type RepositoryContext } from "./abstract";
12
17
  import { CustomerRepository } from "./customer";
@@ -45,6 +50,41 @@ export class MyCustomerRepository extends CustomerRepository {
45
50
  return customer;
46
51
  }
47
52
 
53
+ confirmEmail(
54
+ context: RepositoryContext,
55
+ resetPassword: MyCustomerEmailVerify,
56
+ ) {
57
+ const { tokenValue } = resetPassword;
58
+
59
+ const customerId = validateEmailVerifyToken(tokenValue);
60
+ if (!customerId) {
61
+ throw new CommercetoolsError<ResourceNotFoundError>({
62
+ code: "ResourceNotFound",
63
+ message: `The Customer with ID 'Token(${tokenValue})' was not found.`,
64
+ });
65
+ }
66
+
67
+ const customer = this._storage.get(
68
+ context.projectKey,
69
+ "customer",
70
+ customerId,
71
+ ) as Writable<Customer> | undefined;
72
+
73
+ if (!customer) {
74
+ throw new CommercetoolsError<ResourceNotFoundError>({
75
+ code: "ResourceNotFound",
76
+ message: `The Customer with ID 'Token(${tokenValue})' was not found.`,
77
+ });
78
+ }
79
+
80
+ customer.isEmailVerified = true;
81
+ customer.version += 1;
82
+
83
+ // Update storage
84
+ this._storage.add(context.projectKey, "customer", customer);
85
+ return customer;
86
+ }
87
+
48
88
  deleteMe(context: RepositoryContext): Customer | undefined {
49
89
  // grab the first customer you can find for now. In the future we should
50
90
  // use the customer id from the scope of the token
@@ -177,6 +177,26 @@ describe("Customer Update Actions", () => {
177
177
  expect(response.body.custom.fields.isValidCouponCode).toBe(false);
178
178
  });
179
179
 
180
+ test("setExternalId", async () => {
181
+ assert(customer, "customer not created");
182
+
183
+ customer = {
184
+ ...customer,
185
+ firstName: "John",
186
+ };
187
+ ctMock.project("dummy").add("customer", customer);
188
+
189
+ const response = await supertest(ctMock.app)
190
+ .post(`/dummy/customers/${customer.id}`)
191
+ .send({
192
+ version: 1,
193
+ actions: [{ action: "setExternalId", externalId: "123-xx-123" }],
194
+ });
195
+ expect(response.status).toBe(200);
196
+ expect(response.body.version).toBe(2);
197
+ expect(response.body.externalId).toBe("123-xx-123");
198
+ });
199
+
180
200
  test("setFirstName", async () => {
181
201
  assert(customer, "customer not created");
182
202
 
@@ -217,6 +237,26 @@ describe("Customer Update Actions", () => {
217
237
  expect(response.body.lastName).toBe("Smith");
218
238
  });
219
239
 
240
+ test("setLocale", async () => {
241
+ assert(customer, "customer not created");
242
+
243
+ customer = {
244
+ ...customer,
245
+ salutation: "Mr.",
246
+ };
247
+ ctMock.project("dummy").add("customer", customer);
248
+
249
+ const response = await supertest(ctMock.app)
250
+ .post(`/dummy/customers/${customer.id}`)
251
+ .send({
252
+ version: 1,
253
+ actions: [{ action: "setLocale", locale: "de-DE" }],
254
+ });
255
+ expect(response.status).toBe(200);
256
+ expect(response.body.version).toBe(2);
257
+ expect(response.body.locale).toBe("de-DE");
258
+ });
259
+
220
260
  test("setSalutation", async () => {
221
261
  assert(customer, "customer not created");
222
262
 
@@ -39,5 +39,14 @@ export class CustomerService extends AbstractService {
39
39
  );
40
40
  return response.status(200).send(token);
41
41
  });
42
+
43
+ parent.post("/email-token", (request, response) => {
44
+ const id = request.body.id;
45
+ const token = this.repository.verifyEmailToken(
46
+ getRepositoryContext(request),
47
+ id,
48
+ );
49
+ return response.status(200).send(token);
50
+ });
42
51
  }
43
52
  }
@@ -224,6 +224,53 @@ describe("/me", () => {
224
224
  });
225
225
  });
226
226
 
227
+ test("verify email flow", async () => {
228
+ const customer = {
229
+ ...getBaseResourceProperties(),
230
+ id: "customer-uuid",
231
+ email: "user@example.com",
232
+ password: hashPassword("p4ssw0rd"),
233
+ addresses: [],
234
+ isEmailVerified: false,
235
+ authenticationMode: "Password", //default in Commercetools
236
+ version: 1,
237
+ };
238
+ ctMock.project("dummy").add("customer", customer);
239
+
240
+ const token = await supertest(ctMock.app)
241
+ .post("/dummy/customers/email-token")
242
+ .send({
243
+ id: "customer-uuid",
244
+ })
245
+ .then((response) => response.body as CustomerToken);
246
+
247
+ const response = await supertest(ctMock.app)
248
+ .post("/dummy/me/email/confirm")
249
+ .send({
250
+ tokenValue: token.value,
251
+ });
252
+ expect(response.status).toBe(200);
253
+ });
254
+
255
+ test("fail verify email flow", async () => {
256
+ const response = await supertest(ctMock.app)
257
+ .post("/dummy/me/email/confirm")
258
+ .send({
259
+ tokenValue: "invalid-token",
260
+ });
261
+ expect(response.status).toBe(400);
262
+ expect(response.body).toEqual({
263
+ message: `The Customer with ID 'Token(invalid-token)' was not found.`,
264
+ statusCode: 400,
265
+ errors: [
266
+ {
267
+ code: "ResourceNotFound",
268
+ message: `The Customer with ID 'Token(invalid-token)' was not found.`,
269
+ },
270
+ ],
271
+ });
272
+ });
273
+
227
274
  test("setCustomField", async () => {
228
275
  const response = await supertest(ctMock.app)
229
276
  .post(`/dummy/me`)
@@ -35,6 +35,7 @@ export class MyCustomerService extends AbstractService {
35
35
  router.post("/login", this.signIn.bind(this));
36
36
  router.post("/password", this.changePassword.bind(this));
37
37
  router.post("/password/reset", this.resetPassword.bind(this));
38
+ router.post("/email/confirm", this.emailConfirm.bind(this));
38
39
 
39
40
  parent.use(`/${basePath}`, router);
40
41
  }
@@ -105,6 +106,15 @@ export class MyCustomerService extends AbstractService {
105
106
  return response.status(200).send(customer);
106
107
  }
107
108
 
109
+ emailConfirm(request: Request, response: Response) {
110
+ const customer = this.repository.confirmEmail(
111
+ getRepositoryContext(request),
112
+ request.body,
113
+ );
114
+
115
+ return response.status(200).send(customer);
116
+ }
117
+
108
118
  signIn(request: Request, response: Response) {
109
119
  const { email, password } = request.body;
110
120
  const encodedPassword = hashPassword(password);