ftmocks-utils 1.3.2 → 1.3.4

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/src/log-utils.js CHANGED
@@ -30,7 +30,11 @@ class Logger {
30
30
  time: Date.now(),
31
31
  source: "ftmocks-utils",
32
32
  });
33
- fs.writeFileSync(this.logsFile, JSON.stringify(this.logs, null, 2), "utf8"); // Append the log message to the file
33
+ fs.writeFileSync(
34
+ this.logsFile,
35
+ JSON.stringify(this.logs, null, 2),
36
+ "utf8"
37
+ ); // Append the log message to the file
34
38
  } catch (error) {
35
39
  // Ignore error
36
40
  }
@@ -87,4 +91,13 @@ class Logger {
87
91
  }
88
92
  }
89
93
 
90
- module.exports = Logger;
94
+ const deleteAllLogs = async (ftmocksConifg, testName) => {
95
+ const mockDir = path.join(
96
+ getMockDir(ftmocksConifg),
97
+ `test_${nameToFolder(testName)}`
98
+ );
99
+ const logFilePath = path.join(mockDir, `_logs.json`);
100
+ fs.rmSync(logFilePath, { recursive: true, force: true });
101
+ };
102
+
103
+ module.exports = { Logger, deleteAllLogs };
@@ -0,0 +1,94 @@
1
+ const { compareMockToFetchRequest } = require("./compare-utils");
2
+ const { getCompareRankMockToFetchRequest } = require("./rank-compare-utils");
3
+ const { getMockDir, nameToFolder } = require("./common-utils");
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+
7
+ function getMatchingMockData({
8
+ testMockData,
9
+ defaultMockData,
10
+ url,
11
+ options,
12
+ testConfig,
13
+ testName,
14
+ mode,
15
+ }) {
16
+ let served = false;
17
+ let matchedMocks =
18
+ testMockData?.filter((mock) => {
19
+ if (mock.fileContent.waitForPrevious && !served) {
20
+ return false;
21
+ }
22
+ served = mock.fileContent.served;
23
+ return compareMockToFetchRequest(mock, { url, options });
24
+ }) || [];
25
+ let foundMock = matchedMocks.find((mock) => !mock.fileContent.served)
26
+ ? matchedMocks.find((mock) => !mock.fileContent.served)
27
+ : matchedMocks[matchedMocks.length - 1];
28
+
29
+ if (!foundMock) {
30
+ foundMock = defaultMockData.find((tm) =>
31
+ compareMockToFetchRequest(tm, {
32
+ url,
33
+ options,
34
+ })
35
+ );
36
+ }
37
+
38
+ if (!foundMock && mode !== "strict") {
39
+ const mockRanks = {};
40
+ testMockData.forEach((tm) => {
41
+ const rank = getCompareRankMockToFetchRequest(tm, {
42
+ url,
43
+ options,
44
+ });
45
+ if (rank > 0) {
46
+ mockRanks[tm.id] = rank;
47
+ }
48
+ });
49
+ defaultMockData.forEach((tm) => {
50
+ const rank = getCompareRankMockToFetchRequest(tm, {
51
+ url,
52
+ options,
53
+ });
54
+ if (rank > 0) {
55
+ mockRanks[tm.id] = rank;
56
+ }
57
+ });
58
+ // Sort by rank to find the best match
59
+ const sortedRanks = Object.entries(mockRanks).sort((a, b) => a[1] - b[1]);
60
+ if (sortedRanks.length > 0) {
61
+ const bestMockId = sortedRanks?.[0]?.[0];
62
+ if (bestMockId) {
63
+ foundMock = [...testMockData, ...defaultMockData].find(
64
+ (mock) => mock.id === bestMockId
65
+ );
66
+ }
67
+ }
68
+ }
69
+ // updating stats to mock file
70
+ if (foundMock) {
71
+ let mockFilePath = path.join(
72
+ getMockDir(testConfig),
73
+ `test_${nameToFolder(testName)}`,
74
+ `mock_${foundMock.id}.json`
75
+ );
76
+ if (!fs.existsSync(mockFilePath)) {
77
+ mockFilePath = path.join(
78
+ getMockDir(testConfig),
79
+ "defaultMocks",
80
+ `mock_${foundMock.id}.json`
81
+ );
82
+ }
83
+ foundMock.fileContent.served = true;
84
+ fs.writeFileSync(
85
+ mockFilePath,
86
+ JSON.stringify(foundMock.fileContent, null, 2)
87
+ );
88
+ }
89
+ return foundMock ? foundMock.fileContent : null;
90
+ }
91
+
92
+ module.exports = {
93
+ getMatchingMockData,
94
+ };
@@ -0,0 +1,101 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { getMockDir, nameToFolder } = require("./common-utils");
4
+
5
+ const getDefaultMockDataFromConfig = (testConfig) => {
6
+ const defaultPath = path.join(
7
+ getMockDir(testConfig),
8
+ "defaultMocks",
9
+ "_mock_list.json"
10
+ );
11
+
12
+ try {
13
+ const defaultData = fs.readFileSync(defaultPath, "utf8");
14
+ let parsedData = JSON.parse(defaultData);
15
+
16
+ // Read and attach mock data for each entry in parsedData
17
+ parsedData.forEach((entry) => {
18
+ const mockFilePath = path.join(
19
+ getMockDir(testConfig),
20
+ "defaultMocks",
21
+ `mock_${entry.id}.json`
22
+ );
23
+ try {
24
+ const mockData = fs.readFileSync(mockFilePath, "utf8");
25
+ entry.fileContent = JSON.parse(mockData);
26
+ } catch (error) {
27
+ console.error(`Error reading mock data for ${entry.id}:`, error);
28
+ return entry; // Return the original entry if there's an error
29
+ }
30
+ });
31
+ return parsedData;
32
+ } catch (error) {
33
+ console.error(`Error reading or parsing default mocks:`, error);
34
+ return [];
35
+ }
36
+ };
37
+
38
+ const loadMockDataFromConfig = (testConfig, _testName) => {
39
+ try {
40
+ let testName = _testName;
41
+ if (!testName) {
42
+ // Read the test ID from mockServer.config.json
43
+ const configPath = path.join(
44
+ getMockDir(testConfig),
45
+ "mockServer.config.json"
46
+ );
47
+ const configData = fs.readFileSync(configPath, "utf8");
48
+ const config = JSON.parse(configData);
49
+ testName = config.testName;
50
+ }
51
+ // Read the tests from testConfig
52
+ const mocksPath = path.join(
53
+ getMockDir(testConfig),
54
+ `test_${nameToFolder(testName)}`,
55
+ "_mock_list.json"
56
+ );
57
+ const mocksData = fs.readFileSync(mocksPath, "utf8");
58
+ const mocks = JSON.parse(mocksData);
59
+
60
+ mocks.forEach((mock) => {
61
+ const fileContent = JSON.parse(
62
+ fs.readFileSync(
63
+ path.join(
64
+ getMockDir(testConfig),
65
+ `test_${nameToFolder(testName)}`,
66
+ `mock_${mock.id}.json`
67
+ ),
68
+ "utf8"
69
+ )
70
+ );
71
+ mock.fileContent = fileContent;
72
+ });
73
+
74
+ return mocks;
75
+ } catch (error) {
76
+ console.error("Error loading test data:", error.message);
77
+ return [];
78
+ }
79
+ };
80
+
81
+ async function resetAllMockStats({ testMockData, testConfig, testName }) {
82
+ for (let i = 0; i < testMockData.length; i++) {
83
+ const tmd = testMockData[i];
84
+ const mockFilePath = path.join(
85
+ getMockDir(testConfig),
86
+ `test_${nameToFolder(testName)}`,
87
+ `mock_${tmd.id}.json`
88
+ );
89
+ tmd.fileContent.served = false;
90
+ await fs.writeFileSync(
91
+ mockFilePath,
92
+ JSON.stringify(tmd.fileContent, null, 2)
93
+ );
94
+ }
95
+ }
96
+
97
+ module.exports = {
98
+ getDefaultMockDataFromConfig,
99
+ loadMockDataFromConfig,
100
+ resetAllMockStats,
101
+ };
@@ -0,0 +1,340 @@
1
+ const { getMatchingMockData } = require("./match-utils");
2
+ const { getMockDir, nameToFolder, getHeaders } = require("./common-utils");
3
+ const { getFallbackDir } = require("./common-utils");
4
+ const { getTestByName } = require("./common-utils");
5
+ const { compareMockToMock } = require("./compare-utils");
6
+ const { loadMockDataFromConfig } = require("./mock-utils");
7
+ const { resetAllMockStats } = require("./mock-utils");
8
+ const { getDefaultMockDataFromConfig } = require("./mock-utils");
9
+ const { Logger } = require("./log-utils");
10
+ const { saveIfItIsFile } = require("./file-utils");
11
+ const { createTest } = require("./test-utils");
12
+ const path = require("path");
13
+ const crypto = require("crypto");
14
+ const fs = require("fs");
15
+
16
+ let logger = null;
17
+
18
+ async function initiatePlaywrightRoutes(
19
+ page,
20
+ ftmocksConifg,
21
+ testName,
22
+ mockPath = "**/*",
23
+ excludeMockPath = null
24
+ ) {
25
+ logger = new Logger(
26
+ { disableLogs: ftmocksConifg.DISABLE_LOGS },
27
+ ftmocksConifg,
28
+ testName
29
+ );
30
+ const testMockData = testName
31
+ ? loadMockDataFromConfig(ftmocksConifg, testName)
32
+ : [];
33
+ resetAllMockStats({ testMockData, testConfig: ftmocksConifg, testName });
34
+ const test = await getTestByName(ftmocksConifg, testName);
35
+ const defaultMockData = getDefaultMockDataFromConfig(ftmocksConifg);
36
+ logger.debug("\x1b[32mcalling initiatePlaywrightRoutes fetch\x1b[0m");
37
+ let firstUrl = null;
38
+ await page.route(mockPath, async (route, request) => {
39
+ const url = request.url();
40
+ if (!firstUrl) {
41
+ firstUrl = url;
42
+ }
43
+ const options = {
44
+ url,
45
+ method: request.method(),
46
+ body: request.postData(),
47
+ };
48
+ if (excludeMockPath && new RegExp(excludeMockPath).test(url)) {
49
+ await route.fallback();
50
+ return;
51
+ }
52
+ let mockData = getMatchingMockData({
53
+ testMockData,
54
+ defaultMockData,
55
+ url,
56
+ options,
57
+ testConfig: ftmocksConifg,
58
+ testName,
59
+ mode: test.mode || "loose",
60
+ });
61
+ try {
62
+ if (mockData) {
63
+ const { content, headers, status, file } = mockData.response;
64
+
65
+ const json = {
66
+ status,
67
+ headers: getHeaders(headers),
68
+ body: content,
69
+ };
70
+
71
+ if (file) {
72
+ let filePath = path.join(
73
+ getMockDir(ftmocksConifg),
74
+ `test_${nameToFolder(testName)}`,
75
+ "_files",
76
+ file
77
+ );
78
+ if (!fs.existsSync(filePath)) {
79
+ filePath = path.join(
80
+ getMockDir(ftmocksConifg),
81
+ "defaultMocks",
82
+ "_files",
83
+ file
84
+ );
85
+ }
86
+ if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
87
+ const fileContent = fs.readFileSync(filePath);
88
+ json.body = fileContent;
89
+
90
+ console.debug(
91
+ "\x1b[32mresponse is a file, serving file\x1b[0m",
92
+ filePath,
93
+ url
94
+ );
95
+ await route.fulfill(json);
96
+ }
97
+ } else {
98
+ await route.fulfill(json);
99
+ }
100
+ } else {
101
+ const fallbackDir = getFallbackDir(ftmocksConifg);
102
+ if (!fallbackDir) {
103
+ await route.fallback();
104
+ return;
105
+ }
106
+ const urlObj = new URL(route.request().url());
107
+ let filePath = path.join(
108
+ fallbackDir,
109
+ urlObj.pathname === "/" || urlObj.pathname === ""
110
+ ? ftmocksConifg.FALLBACK_DIR_INDEX_FILE || "index.html"
111
+ : urlObj.pathname
112
+ );
113
+
114
+ if (
115
+ !fs.existsSync(filePath) &&
116
+ !path.extname(filePath) &&
117
+ url === firstUrl
118
+ ) {
119
+ filePath = path.join(
120
+ fallbackDir,
121
+ ftmocksConifg.FALLBACK_DIR_INDEX_FILE_FOR_STATUS_404 || "index.html"
122
+ );
123
+ logger.debug(
124
+ "\x1b[32mserving file for status 404\x1b[0m",
125
+ filePath,
126
+ url
127
+ );
128
+ }
129
+ if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
130
+ const fileContent = fs.readFileSync(filePath);
131
+ const ext = path.extname(filePath);
132
+ const contentType =
133
+ {
134
+ ".html": "text/html",
135
+ ".htm": "text/html",
136
+ ".xhtml": "application/xhtml+xml",
137
+ ".css": "text/css",
138
+ ".js": "application/javascript",
139
+ ".json": "application/json",
140
+ ".png": "image/png",
141
+ ".jpg": "image/jpeg",
142
+ ".svg": "image/svg+xml",
143
+ ".ico": "image/x-icon",
144
+ ".webp": "image/webp",
145
+ ".mp4": "video/mp4",
146
+ ".mp3": "audio/mpeg",
147
+ ".wav": "audio/wav",
148
+ ".ogg": "audio/ogg",
149
+ ".pdf": "application/pdf",
150
+ ".doc": "application/msword",
151
+ ".docx":
152
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
153
+ ".xls": "application/vnd.ms-excel",
154
+ ".xlsx":
155
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
156
+ ".ppt": "application/vnd.ms-powerpoint",
157
+ ".pptx":
158
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
159
+ ".zip": "application/zip",
160
+ ".rar": "application/x-rar-compressed",
161
+ ".7z": "application/x-7z-compressed",
162
+ ".tar": "application/x-tar",
163
+ ".gz": "application/gzip",
164
+ ".bz2": "application/x-bzip2",
165
+ ".xz": "application/x-xz",
166
+ ".exe": "application/x-msdownload",
167
+ ".dll": "application/x-msdownload",
168
+ ".so": "application/x-sharedlib",
169
+ ".dylib": "application/x-dynamiclib",
170
+ ".bin": "application/octet-stream",
171
+ ".txt": "text/plain",
172
+ ".csv": "text/csv",
173
+ ".tsv": "text/tab-separated-values",
174
+ ".xml": "application/xml",
175
+ ".xsl": "application/xml",
176
+ ".xslt": "application/xml",
177
+ ".xlt": "application/xml",
178
+ ".xltx": "application/xml",
179
+ ".xltm": "application/xml",
180
+ ".yaml": "text/yaml",
181
+ ".yml": "text/yaml",
182
+ ".toml": "text/toml",
183
+ ".php": "application/x-httpd-php",
184
+ }[ext] || "application/octet-stream";
185
+
186
+ logger.info("\x1b[32mserving file\x1b[0m", filePath);
187
+ await route.fulfill({
188
+ body: fileContent,
189
+ headers: { "Content-Type": contentType },
190
+ });
191
+ } else {
192
+ logger.debug("\x1b[31mmissing mock data, falling back\x1b[0m", url);
193
+ await route.fallback();
194
+ }
195
+ }
196
+ } catch (e) {
197
+ logger.error(e);
198
+ logger.error(
199
+ "\x1b[31merror at initiatePlaywrightRoutes\x1b[0m",
200
+ url,
201
+ options
202
+ );
203
+ }
204
+ });
205
+ }
206
+
207
+ async function recordPlaywrightRoutes(
208
+ page,
209
+ ftmocksConifg,
210
+ config = {
211
+ testName,
212
+ mockPath: "**/*",
213
+ pattern: "^/api/.*",
214
+ avoidDuplicatesInTheTest: false,
215
+ avoidDuplicatesWithDefaultMocks: false,
216
+ }
217
+ ) {
218
+ await page.route(config.mockPath, async (route) => {
219
+ try {
220
+ const urlObj = new URL(route.request().url());
221
+ if (config.pattern && config.pattern.length > 0) {
222
+ const patternRegex = new RegExp(config.pattern);
223
+ if (!patternRegex.test(urlObj.pathname)) {
224
+ await route.continue();
225
+ return;
226
+ }
227
+ }
228
+
229
+ const test = await getTestByName(ftmocksConifg, config.testName);
230
+ if (!test) {
231
+ await createTest(ftmocksConifg, config.testName);
232
+ }
233
+
234
+ if (await saveIfItIsFile(route, config.testName, ftmocksConifg)) {
235
+ return;
236
+ }
237
+
238
+ const mockData = {
239
+ url: urlObj.pathname + urlObj.search,
240
+ time: new Date().toString(),
241
+ method: route.request().method(),
242
+ request: {
243
+ headers: await route.request().headers(),
244
+ queryString: Array.from(urlObj.searchParams.entries()).map(
245
+ ([name, value]) => ({
246
+ name,
247
+ value,
248
+ })
249
+ ),
250
+ postData: route.request().postData()
251
+ ? {
252
+ mimeType: "application/json",
253
+ text: route.request().postData(),
254
+ }
255
+ : null,
256
+ },
257
+ response: {
258
+ status: (await route.fetch()).status(),
259
+ headers: (await route.fetch()).headers(),
260
+ content: await (await route.fetch()).text(),
261
+ },
262
+ id: crypto.randomUUID(),
263
+ served: false,
264
+ ignoreParams: ftmocksConifg.ignoreParams || [],
265
+ };
266
+
267
+ await createTest(ftmocksConifg, config.testName);
268
+ if (config.avoidDuplicatesInTheTest) {
269
+ // Check if the mock data is a duplicate of a mock data in the test
270
+ const testMockList = loadMockDataFromConfig(
271
+ ftmocksConifg,
272
+ config.testName
273
+ );
274
+ const matchResponse = testMockList.find((mock) =>
275
+ compareMockToMock(mock.fileContent, mockData, true)
276
+ );
277
+ if (matchResponse) {
278
+ console.log("Aborting duplicate mock data in the test");
279
+ await route.continue();
280
+ return;
281
+ }
282
+ }
283
+
284
+ if (config.avoidDuplicatesWithDefaultMocks) {
285
+ // Check if the mock data is a duplicate of a mock data in the test
286
+ const defaultMockList = getDefaultMockDataFromConfig(ftmocksConifg);
287
+ const matchResponse = defaultMockList.find((mock) =>
288
+ compareMockToMock(mock.fileContent, mockData, true)
289
+ );
290
+ if (matchResponse) {
291
+ console.log("Aborting duplicate mock data with default mocks");
292
+ await route.continue();
293
+ return;
294
+ }
295
+ }
296
+
297
+ // Save the mock data to the test
298
+ const mockListPath = path.join(
299
+ getMockDir(ftmocksConifg),
300
+ `test_${nameToFolder(config.testName)}`,
301
+ "_mock_list.json"
302
+ );
303
+ let mockList = [];
304
+ if (fs.existsSync(mockListPath)) {
305
+ mockList = JSON.parse(fs.readFileSync(mockListPath, "utf8"));
306
+ }
307
+ mockList.push({
308
+ id: mockData.id,
309
+ url: mockData.url,
310
+ method: mockData.method,
311
+ time: mockData.time,
312
+ });
313
+
314
+ // Create test directory if it doesn't exist
315
+ const testDir = path.join(
316
+ getMockDir(ftmocksConifg),
317
+ `test_${nameToFolder(config.testName)}`
318
+ );
319
+ if (!fs.existsSync(testDir)) {
320
+ fs.mkdirSync(testDir, { recursive: true });
321
+ }
322
+ fs.writeFileSync(mockListPath, JSON.stringify(mockList, null, 2));
323
+ const mocDataPath = path.join(
324
+ getMockDir(ftmocksConifg),
325
+ `test_${nameToFolder(config.testName)}`,
326
+ `mock_${mockData.id}.json`
327
+ );
328
+ fs.writeFileSync(mocDataPath, JSON.stringify(mockData, null, 2));
329
+ await route.continue();
330
+ } catch (error) {
331
+ console.error(error);
332
+ await route.continue();
333
+ }
334
+ });
335
+ }
336
+
337
+ module.exports = {
338
+ initiatePlaywrightRoutes,
339
+ recordPlaywrightRoutes,
340
+ };
@@ -0,0 +1,62 @@
1
+ const { clearNulls, charDifference, processURL } = require("./common-utils");
2
+ const { FtJSON } = require("./json-utils");
3
+ const { isUrlAndMethodSame } = require("./compare-utils");
4
+
5
+ const getSameRequestRank = (req1, req2) => {
6
+ let rank = 1;
7
+ clearNulls(req1.postData);
8
+ clearNulls(req2.postData);
9
+ // Compare query strings
10
+ const queryDiff = charDifference(
11
+ req1.url.split("?")[1] || "",
12
+ req2.url.split("?")[1] || ""
13
+ );
14
+ rank = rank + queryDiff;
15
+ // Compare post data
16
+ const postDataDiff = charDifference(
17
+ FtJSON.stringify(req1.postData || {}),
18
+ FtJSON.stringify(req2.postData || {})
19
+ );
20
+ rank = rank + postDataDiff;
21
+ return rank;
22
+ };
23
+
24
+ function getCompareRankMockToFetchRequest(mock, fetchReq) {
25
+ try {
26
+ const mockURL = processURL(
27
+ mock.fileContent.url,
28
+ mock.fileContent.ignoreParams
29
+ );
30
+ const reqURL = processURL(fetchReq.url, mock.fileContent.ignoreParams);
31
+ if (
32
+ !isUrlAndMethodSame(
33
+ { url: mockURL, method: mock.fileContent.method },
34
+ { url: reqURL, method: fetchReq.options.method || "GET" }
35
+ )
36
+ ) {
37
+ return 0;
38
+ }
39
+ const postData = mock.fileContent.request?.postData?.text
40
+ ? FtJSON.parse(mock.fileContent.request?.postData?.text)
41
+ : mock.fileContent.request?.postData;
42
+ return getSameRequestRank(
43
+ { url: mockURL, method: mock.fileContent.method, postData },
44
+ {
45
+ method: fetchReq.options.method || "GET",
46
+ postData: fetchReq.options.body?.length
47
+ ? FtJSON.parse(fetchReq.options.body)
48
+ : fetchReq.options.body,
49
+ url: reqURL,
50
+ }
51
+ );
52
+ } catch (e) {
53
+ console.error("error at getCompareRankMockToFetchRequest", mock, fetchReq);
54
+ console.error(e);
55
+ }
56
+ return false;
57
+ }
58
+
59
+ module.exports = {
60
+ getSameRequestRank,
61
+ getCompareRankMockToFetchRequest,
62
+ };