@reactionary/commercetools 0.10.44 → 0.10.46

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.
@@ -13,7 +13,7 @@ class CommercetoolsOrderSearchFactory {
13
13
  pageNumber: (Math.ceil(data.offset / data.limit) || 0) + 1,
14
14
  pageSize: data.limit,
15
15
  totalCount: data.total || 0,
16
- totalPages: Math.ceil((data.total || 0) / data.limit || 0) + 1,
16
+ totalPages: data.limit > 0 ? Math.ceil((data.total || 0) / data.limit) : 0,
17
17
  items: data.results.map((order) => this.parseOrderSearchItem(context, order))
18
18
  };
19
19
  return this.orderSearchResultSchema.parse(result);
@@ -38,7 +38,7 @@ class CommercetoolsProductSearchFactory {
38
38
  pageNumber: (Math.ceil(data.offset / data.limit) || 0) + 1,
39
39
  pageSize: data.limit,
40
40
  totalCount: data.total || 0,
41
- totalPages: Math.ceil((data.total || 0) / data.limit || 0) + 1,
41
+ totalPages: data.limit > 0 ? Math.ceil((data.total || 0) / data.limit) : 0,
42
42
  items,
43
43
  facets
44
44
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/commercetools",
3
- "version": "0.10.44",
3
+ "version": "0.10.46",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./src/index.d.ts",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/Solteq/reactionary.git"
10
10
  },
11
11
  "dependencies": {
12
- "@reactionary/core": "0.10.44",
12
+ "@reactionary/core": "0.10.46",
13
13
  "zod": "4.1.9",
14
14
  "@commercetools/ts-client": "^4.9.1",
15
15
  "@commercetools/platform-sdk": "^8.25.0",
@@ -0,0 +1,14 @@
1
+ import type { CommercetoolsConfiguration } from '../schema/configuration.schema.js';
2
+ /**
3
+ * Record/replay of Commercetools HTTP traffic, so specs can run offline.
4
+ *
5
+ * By default, specs replay the interactions stored under __fixtures__/<suite>/<test>.json
6
+ * against a fake configuration. Run with CT_RECORD=1 to execute against the live project
7
+ * configured in the repository's .test.env and (re-)record the fixtures instead.
8
+ */
9
+ export declare const isRecording: boolean;
10
+ /**
11
+ * Registers record/replay hooks for the surrounding describe block and returns
12
+ * the configuration that specs should construct their clients with.
13
+ */
14
+ export declare function useCommercetoolsFixtures(suite: string): CommercetoolsConfiguration;
@@ -0,0 +1,125 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { config as loadEnv } from "dotenv";
3
+ import { afterEach, beforeEach, vi } from "vitest";
4
+ import { getCommercetoolsTestConfiguration } from "./test-utils.js";
5
+ const isRecording = process.env["CT_RECORD"] === "1";
6
+ const FIXTURES = new URL("./__fixtures__/", import.meta.url);
7
+ const TEST_ENV = new URL("../../../../.test.env", import.meta.url);
8
+ const REDACTED_KEYS = /* @__PURE__ */ new Set(["access_token", "refresh_token", "client_id", "clientId"]);
9
+ const realFetch = globalThis.fetch;
10
+ function getFixtureConfiguration() {
11
+ if (isRecording) {
12
+ loadEnv({ path: TEST_ENV.pathname, quiet: true });
13
+ return getCommercetoolsTestConfiguration();
14
+ }
15
+ return {
16
+ ...getCommercetoolsTestConfiguration(),
17
+ apiUrl: "https://api.commercetools.test",
18
+ authUrl: "https://auth.commercetools.test",
19
+ clientId: "test-client-id",
20
+ clientSecret: "test-client-secret",
21
+ projectKey: "test-project",
22
+ scopes: []
23
+ };
24
+ }
25
+ function slugify(name) {
26
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
27
+ }
28
+ function sanitize(value, projectKey) {
29
+ if (Array.isArray(value)) {
30
+ return value.map((entry) => sanitize(entry, projectKey));
31
+ }
32
+ if (value && typeof value === "object") {
33
+ return Object.fromEntries(
34
+ Object.entries(value).map(([key, entry]) => {
35
+ if (typeof entry === "string" && REDACTED_KEYS.has(key)) {
36
+ return [key, `redacted-${key}`];
37
+ }
38
+ if (typeof entry === "string" && key === "scope") {
39
+ return [key, entry.replaceAll(`:${projectKey}`, ":{projectKey}")];
40
+ }
41
+ return [key, sanitize(entry, projectKey)];
42
+ })
43
+ );
44
+ }
45
+ return value;
46
+ }
47
+ function describeRequest(input, init) {
48
+ const url = input instanceof Request ? input.url : input.toString();
49
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
50
+ return { url, method };
51
+ }
52
+ function useCommercetoolsFixtures(suite) {
53
+ const config = getFixtureConfiguration();
54
+ const projectSegment = new RegExp(`/${config.projectKey}(?=/|\\?|$)`, "g");
55
+ const normalizeUrl = (url) => url.replace(config.authUrl, "{authUrl}").replace(config.apiUrl, "{apiUrl}").replace(projectSegment, "/{projectKey}");
56
+ let cassettePath;
57
+ let interactions = [];
58
+ let consumed = /* @__PURE__ */ new Set();
59
+ const recordingFetch = async (input, init) => {
60
+ const { url, method } = describeRequest(input, init);
61
+ const response = await realFetch(input, init);
62
+ const contentType = response.headers.get("content-type");
63
+ const text = await response.clone().text();
64
+ const isJson = contentType?.includes("json") ?? false;
65
+ interactions.push({
66
+ method,
67
+ url: normalizeUrl(url),
68
+ status: response.status,
69
+ contentType,
70
+ body: isJson && text ? sanitize(JSON.parse(text), config.projectKey) : text
71
+ });
72
+ return response;
73
+ };
74
+ const replayingFetch = async (input, init) => {
75
+ const { url, method } = describeRequest(input, init);
76
+ const normalized = normalizeUrl(url);
77
+ const index = interactions.findIndex(
78
+ (interaction2, i) => !consumed.has(i) && interaction2.method === method && interaction2.url === normalized
79
+ );
80
+ if (index === -1) {
81
+ throw new Error(
82
+ `No recorded Commercetools interaction for ${method} ${normalized} in ${cassettePath.pathname}. Re-record the fixtures with CT_RECORD=1.`
83
+ );
84
+ }
85
+ consumed.add(index);
86
+ const interaction = interactions[index];
87
+ const body = typeof interaction.body === "string" ? interaction.body : JSON.stringify(interaction.body);
88
+ return new Response(body || null, {
89
+ status: interaction.status,
90
+ headers: interaction.contentType ? { "content-type": interaction.contentType } : {}
91
+ });
92
+ };
93
+ beforeEach((context) => {
94
+ cassettePath = new URL(`${suite}/${slugify(context.task.name)}.json`, FIXTURES);
95
+ consumed = /* @__PURE__ */ new Set();
96
+ if (isRecording) {
97
+ interactions = [];
98
+ } else {
99
+ const cassette = existsSync(cassettePath) ? JSON.parse(readFileSync(cassettePath, "utf8")) : { interactions: [] };
100
+ interactions = cassette.interactions;
101
+ }
102
+ vi.stubGlobal("fetch", isRecording ? recordingFetch : replayingFetch);
103
+ });
104
+ afterEach(() => {
105
+ vi.unstubAllGlobals();
106
+ if (isRecording) {
107
+ if (interactions.length > 0) {
108
+ mkdirSync(new URL(".", cassettePath), { recursive: true });
109
+ writeFileSync(cassettePath, JSON.stringify({ interactions }, null, 2) + "\n");
110
+ }
111
+ return;
112
+ }
113
+ const unused = interactions.filter((_, i) => !consumed.has(i));
114
+ if (unused.length > 0) {
115
+ throw new Error(
116
+ `Recorded Commercetools interactions were not requested: ` + unused.map((interaction) => `${interaction.method} ${interaction.url}`).join(", ")
117
+ );
118
+ }
119
+ });
120
+ return config;
121
+ }
122
+ export {
123
+ isRecording,
124
+ useCommercetoolsFixtures
125
+ };