@labdigital/commercetools-mock 2.35.0 → 2.37.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.35.0",
3
+ "version": "2.37.0",
4
4
  "license": "MIT",
5
5
  "author": "Michael van Tellingen",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "devDependencies": {
36
36
  "@changesets/changelog-github": "^0.5.0",
37
37
  "@changesets/cli": "^2.27.1",
38
- "@commercetools/platform-sdk": "7.11.0",
38
+ "@commercetools/platform-sdk": "7.17.0",
39
39
  "@stylistic/eslint-plugin": "^1.6.2",
40
40
  "@types/basic-auth": "^1.1.8",
41
41
  "@types/body-parser": "^1.19.5",
@@ -130,4 +130,46 @@ describe("OAuth2Server", () => {
130
130
  });
131
131
  });
132
132
  });
133
+
134
+ describe("POST /:projectKey/in-store/key=:storeKey/customers/token", () => {
135
+ it("should return a token for in-store customer access", async () => {
136
+ const projectKey = "test-project";
137
+ const storeKey = "test-store";
138
+
139
+ storage.add(projectKey, "customer", {
140
+ ...getBaseResourceProperties(),
141
+ email: "j.doe@example.org",
142
+ password: hashPassword("password"),
143
+ addresses: [],
144
+ authenticationMode: "password",
145
+ isEmailVerified: true,
146
+ stores: [
147
+ {
148
+ typeId: "store",
149
+ key: storeKey,
150
+ },
151
+ ],
152
+ });
153
+
154
+ const response = await supertest(app)
155
+ .post(`/${projectKey}/in-store/key=${storeKey}/customers/token`)
156
+ .auth("validClientId", "validClientSecret")
157
+ .query({
158
+ grant_type: "password",
159
+ username: "j.doe@example.org",
160
+ password: "password",
161
+ scope: `${projectKey}:manage_my_profile`,
162
+ })
163
+ .send();
164
+
165
+ expect(response.status).toBe(200);
166
+ expect(response.body).toEqual({
167
+ scope: expect.stringMatching(/customer_id:([^\s]+)/),
168
+ access_token: expect.stringMatching(/\S{8,}==$/),
169
+ refresh_token: expect.stringMatching(/test-project:\S{8,}==$/),
170
+ expires_in: 172800,
171
+ token_type: "Bearer",
172
+ });
173
+ });
174
+ });
133
175
  });
@@ -287,15 +287,52 @@ export class OAuth2Server {
287
287
  response: Response,
288
288
  next: NextFunction,
289
289
  ) {
290
- return next(
291
- new CommercetoolsError<InvalidClientError>(
290
+ const projectKey = request.params.projectKey;
291
+ const storeKey = request.params.storeKey;
292
+ const grantType = request.query.grant_type || request.body.grant_type;
293
+ if (!grantType) {
294
+ return next(
295
+ new CommercetoolsError<InvalidRequestError>(
296
+ {
297
+ code: "invalid_request",
298
+ message: "Missing required parameter: grant_type.",
299
+ },
300
+ 400,
301
+ ),
302
+ );
303
+ }
304
+
305
+ if (grantType === "password") {
306
+ const username = request.query.username || request.body.username;
307
+ const password = hashPassword(
308
+ request.query.password || request.body.password,
309
+ );
310
+ const scope =
311
+ request.query.scope?.toString() || request.body.scope?.toString();
312
+
313
+ const result = this.customerRepository.query(
314
+ { projectKey, storeKey },
292
315
  {
293
- code: "invalid_client",
294
- message: "Not implemented yet in commercetools-mock",
316
+ where: [`email = "${username}"`, `password = "${password}"`],
295
317
  },
296
- 401,
297
- ),
298
- );
318
+ );
319
+
320
+ if (result.count === 0) {
321
+ return next(
322
+ new CommercetoolsError<any>(
323
+ {
324
+ code: "invalid_customer_account_credentials",
325
+ message: "Customer account with the given credentials not found.",
326
+ },
327
+ 400,
328
+ ),
329
+ );
330
+ }
331
+
332
+ const customer = result.results[0];
333
+ const token = this.store.getCustomerToken(projectKey, customer.id, scope);
334
+ return response.status(200).send(token);
335
+ }
299
336
  }
300
337
 
301
338
  async anonymousTokenHandler(
@@ -1,10 +1,12 @@
1
1
  import type {
2
+ Address,
2
3
  Customer,
3
4
  CustomerCreatePasswordResetToken,
4
5
  CustomerDraft,
5
6
  CustomerResetPassword,
6
7
  CustomerToken,
7
8
  DuplicateFieldError,
9
+ InvalidInputError,
8
10
  MyCustomerResetPassword,
9
11
  ResourceNotFoundError,
10
12
  } from "@commercetools/platform-sdk";
@@ -53,20 +55,50 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
53
55
  });
54
56
  }
55
57
 
56
- const addresses =
58
+ const addresses: Address[] =
57
59
  draft.addresses?.map((address) => ({
58
60
  ...address,
59
61
  id: generateRandomString(5),
60
62
  })) ?? [];
61
63
 
62
- const defaultBillingAddressId =
63
- addresses.length > 0 && draft.defaultBillingAddress !== undefined
64
- ? addresses[draft.defaultBillingAddress].id
65
- : undefined;
66
- const defaultShippingAddressId =
67
- addresses.length > 0 && draft.defaultShippingAddress !== undefined
68
- ? addresses[draft.defaultShippingAddress].id
69
- : undefined;
64
+ const lookupAdressId = (
65
+ addresses: Address[],
66
+ addressId: number,
67
+ ): string => {
68
+ if (addressId < addresses.length) {
69
+ const id = addresses[addressId].id;
70
+ if (!id) {
71
+ throw new Error("Address ID is missing");
72
+ }
73
+ return id;
74
+ }
75
+ throw new CommercetoolsError<InvalidInputError>({
76
+ code: "InvalidInput",
77
+ message: `Address with ID '${addressId}' not found.`,
78
+ errors: [
79
+ {
80
+ code: "InvalidInput",
81
+ message: `Address with ID '${addressId}' not found.`,
82
+ field: "addressId",
83
+ },
84
+ ],
85
+ });
86
+ };
87
+
88
+ const defaultBillingAddressId = draft.defaultBillingAddress
89
+ ? lookupAdressId(addresses, draft.defaultBillingAddress)
90
+ : undefined;
91
+ const defaultShippingAddressId = draft.defaultShippingAddress
92
+ ? lookupAdressId(addresses, draft.defaultShippingAddress)
93
+ : undefined;
94
+ const shippingAddressIds =
95
+ draft.shippingAddresses?.map((addressId) =>
96
+ lookupAdressId(addresses, addressId),
97
+ ) ?? [];
98
+ const billingAddressIds =
99
+ draft.billingAddresses?.map((addressId) =>
100
+ lookupAdressId(addresses, addressId),
101
+ ) ?? [];
70
102
 
71
103
  const resource: Customer = {
72
104
  ...getBaseResourceProperties(),
@@ -86,6 +118,8 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
86
118
  externalId: draft.externalId,
87
119
  defaultBillingAddressId: defaultBillingAddressId,
88
120
  defaultShippingAddressId: defaultShippingAddressId,
121
+ shippingAddressIds: shippingAddressIds,
122
+ billingAddressIds: billingAddressIds,
89
123
  custom: createCustomFields(
90
124
  draft.custom,
91
125
  context.projectKey,
@@ -5,6 +5,7 @@ import type {
5
5
  ProjectChangeCountriesAction,
6
6
  ProjectChangeCountryTaxRateFallbackEnabledAction,
7
7
  ProjectChangeCurrenciesAction,
8
+ ProjectChangeCustomerSearchStatusAction,
8
9
  ProjectChangeLanguagesAction,
9
10
  ProjectChangeMessagesConfigurationAction,
10
11
  ProjectChangeNameAction,
@@ -95,6 +96,18 @@ class ProjectUpdateHandler
95
96
  resource.currencies = currencies;
96
97
  }
97
98
 
99
+ changeCustomerSearchStatus(
100
+ context: RepositoryContext,
101
+ resource: Writable<Project>,
102
+ { status }: ProjectChangeCustomerSearchStatusAction,
103
+ ) {
104
+ if (!resource.searchIndexing?.customers) {
105
+ throw new Error("Invalid project state");
106
+ }
107
+ resource.searchIndexing.customers.status = status;
108
+ resource.searchIndexing.customers.lastModifiedAt = new Date().toISOString();
109
+ }
110
+
98
111
  changeLanguages(
99
112
  context: RepositoryContext,
100
113
  resource: Writable<Project>,
@@ -150,8 +163,20 @@ class ProjectUpdateHandler
150
163
  changeProductSearchIndexingEnabled(
151
164
  context: RepositoryContext,
152
165
  resource: Writable<Project>,
153
- { enabled }: ProjectChangeProductSearchIndexingEnabledAction,
166
+ { enabled, mode }: ProjectChangeProductSearchIndexingEnabledAction,
154
167
  ) {
168
+ if (mode === "ProductsSearch") {
169
+ if (!resource.searchIndexing?.productsSearch) {
170
+ throw new Error("Invalid project state");
171
+ }
172
+ resource.searchIndexing.productsSearch.status = enabled
173
+ ? "Activated"
174
+ : "Deactivated";
175
+ resource.searchIndexing.productsSearch.lastModifiedAt =
176
+ new Date().toISOString();
177
+ return;
178
+ }
179
+
155
180
  if (!resource.searchIndexing?.products) {
156
181
  throw new Error("Invalid project state");
157
182
  }
@@ -1,4 +1,8 @@
1
- import { Customer, CustomerToken } from "@commercetools/platform-sdk";
1
+ import {
2
+ Customer,
3
+ CustomerDraft,
4
+ CustomerToken,
5
+ } from "@commercetools/platform-sdk";
2
6
  import assert from "assert";
3
7
  import supertest from "supertest";
4
8
  import { afterEach, beforeEach, describe, expect, test } from "vitest";
@@ -7,6 +11,46 @@ import { CommercetoolsMock, getBaseResourceProperties } from "../index";
7
11
 
8
12
  const ctMock = new CommercetoolsMock();
9
13
 
14
+ afterEach(() => {
15
+ ctMock.clear();
16
+ });
17
+
18
+ describe("Customer create", () => {
19
+ test("create new customer", async () => {
20
+ const draft: CustomerDraft = {
21
+ email: "new-user@example.com",
22
+ password: "supersecret",
23
+ authenticationMode: "Password",
24
+ stores: [],
25
+ addresses: [
26
+ {
27
+ key: "address-key",
28
+ firstName: "John",
29
+ lastName: "Doe",
30
+ streetName: "Main Street",
31
+ streetNumber: "1",
32
+ postalCode: "12345",
33
+ country: "DE",
34
+ },
35
+ ],
36
+ billingAddresses: [0],
37
+ shippingAddresses: [0],
38
+ };
39
+
40
+ const response = await supertest(ctMock.app)
41
+ .post(`/dummy/customers`)
42
+ .send(draft);
43
+
44
+ const customer = response.body.customer as Customer;
45
+ expect(response.status, JSON.stringify(customer)).toBe(201);
46
+ expect(customer.version).toBe(1);
47
+ expect(customer.defaultBillingAddressId).toBeUndefined();
48
+ expect(customer.defaultShippingAddressId).toBeUndefined();
49
+ expect(customer.billingAddressIds).toHaveLength(1);
50
+ expect(customer.shippingAddressIds).toHaveLength(1);
51
+ });
52
+ });
53
+
10
54
  describe("Customer Update Actions", () => {
11
55
  let customer: Customer | undefined;
12
56
 
@@ -25,10 +69,6 @@ describe("Customer Update Actions", () => {
25
69
  ctMock.project("dummy").add("customer", customer);
26
70
  });
27
71
 
28
- afterEach(() => {
29
- ctMock.clear();
30
- });
31
-
32
72
  test("exists", async () => {
33
73
  assert(customer, "customer not created");
34
74
 
@@ -12,6 +12,7 @@ import { CustomerGroupService } from "./customer-group";
12
12
  import { DiscountCodeService } from "./discount-code";
13
13
  import { ExtensionServices } from "./extension";
14
14
  import { InventoryEntryService } from "./inventory-entry";
15
+ import { MyBusinessUnitService } from "./my-business-unit";
15
16
  import { MyCartService } from "./my-cart";
16
17
  import { MyCustomerService } from "./my-customer";
17
18
  import { MyOrderService } from "./my-order";
@@ -66,6 +67,7 @@ export const createServices = (
66
67
  "my-cart": new MyCartService(router, repos["my-cart"]),
67
68
  "my-order": new MyOrderService(router, repos["my-order"]),
68
69
  "my-customer": new MyCustomerService(router, repos["my-customer"]),
70
+ "my-business-unit": new MyBusinessUnitService(router, repos["business-unit"]),
69
71
  "my-payment": new MyPaymentService(router, repos["my-payment"]),
70
72
  "my-shopping-list": new MyShoppingListService(
71
73
  router,
@@ -0,0 +1,28 @@
1
+ import { Router } from "express";
2
+ import { BusinessUnitRepository } from "~src/repositories/business-unit";
3
+ import AbstractService from "./abstract";
4
+
5
+ export class MyBusinessUnitService extends AbstractService {
6
+ public repository: BusinessUnitRepository;
7
+
8
+ constructor(parent: Router, repository: BusinessUnitRepository) {
9
+ super(parent);
10
+ this.repository = repository;
11
+ }
12
+
13
+ getBasePath() {
14
+ return "me";
15
+ }
16
+
17
+ registerRoutes(parent: Router) {
18
+ // Overwrite this function to be able to handle /me/business-units path.
19
+ const basePath = this.getBasePath();
20
+ const router = Router({ mergeParams: true });
21
+
22
+ this.extraRoutes(router);
23
+
24
+ router.get("/business-units/", this.get.bind(this));
25
+
26
+ parent.use(`/${basePath}`, router);
27
+ }
28
+ }
@@ -35,6 +35,8 @@ describe("Me", () => {
35
35
  version: 1,
36
36
  isEmailVerified: false,
37
37
  addresses: [],
38
+ billingAddressIds: [],
39
+ shippingAddressIds: [],
38
40
  id: expect.anything(),
39
41
  createdAt: expect.anything(),
40
42
  lastModifiedAt: expect.anything(),
@@ -39,12 +39,18 @@ describe("Project", () => {
39
39
  },
40
40
  name: "",
41
41
  searchIndexing: {
42
+ customers: {
43
+ status: "Deactivated",
44
+ },
42
45
  orders: {
43
46
  status: "Deactivated",
44
47
  },
45
48
  products: {
46
49
  status: "Deactivated",
47
50
  },
51
+ productsSearch: {
52
+ status: "Deactivated",
53
+ },
48
54
  },
49
55
  trialUntil: "2018-12",
50
56
  } as Project);
@@ -87,9 +87,15 @@ export class InMemoryStorage extends AbstractStorage {
87
87
  products: {
88
88
  status: "Deactivated",
89
89
  },
90
+ productsSearch: {
91
+ status: "Deactivated",
92
+ },
90
93
  orders: {
91
94
  status: "Deactivated",
92
95
  },
96
+ customers: {
97
+ status: "Deactivated",
98
+ },
93
99
  },
94
100
  version: 1,
95
101
  };
package/src/types.ts CHANGED
@@ -14,7 +14,8 @@ export type ServiceTypes =
14
14
  | "my-cart"
15
15
  | "my-order"
16
16
  | "my-payment"
17
- | "my-customer";
17
+ | "my-customer"
18
+ | "my-business-unit";
18
19
 
19
20
  export type Services = Partial<{
20
21
  [index in ServiceTypes]: AbstractService;