@jaypie/testkit 1.0.19 → 1.0.21

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/README.md CHANGED
@@ -12,21 +12,33 @@ npm install --save-dev @jaypie/testkit
12
12
 
13
13
  ### Example
14
14
 
15
+ #### Mocking Jaypie
16
+
17
+ The testkit provides a complete mock for Jaypie including:
18
+
19
+ * Log spying (`expect(log.warn).toHaveBeenCalled()`)
20
+ * Default responses for runtime-only functions (`connect`, `sendMessage`, `submitMetric`)
21
+ * No automatic error handling for handlers (which is good in production but obfuscates tests)
22
+ * Most non-utility functions are mocked to allow simple testing
23
+
24
+ ```javascript
25
+ vi.mock("jaypie", vi.importActual("@jaypie/testkit"));
26
+ ```
27
+
15
28
  #### Log Spying
16
29
 
17
30
  ```javascript
18
- import { restoreLog, spyLog } from "@jaypie/testkit";
19
- import { log } from "@jaypie/core";
31
+ import { log } from "jaypie";
32
+
33
+ vi.mock("jaypie", vi.importActual("@jaypie/testkit"));
20
34
 
21
- beforeEach(() => {
22
- spyLog(log);
23
- });
24
35
  afterEach(() => {
25
- restoreLog(log);
26
36
  vi.clearAllMocks();
27
37
  });
28
38
 
29
39
  test("log", () => {
40
+ expect(vi.isMockFunction(log.warn)).toBe(true);
41
+ expect(log.warn).not.toHaveBeenCalled();
30
42
  log.warn("Danger");
31
43
  expect(log.warn).toHaveBeenCalled();
32
44
  expect(log.error).not.toHaveBeenCalled();
@@ -117,6 +129,30 @@ A [JSON Schema](https://json-schema.org/) validator for the [JSON:API](https://j
117
129
 
118
130
  ### `matchers`
119
131
 
132
+ ```javascript
133
+ export default {
134
+ toBeCalledWithInitialParams,
135
+ toBeClass,
136
+ toBeJaypieError,
137
+ toBeValidSchema: jsonSchemaMatchers.toBeValidSchema,
138
+ toMatchBase64,
139
+ toMatchJwt,
140
+ toMatchMongoId,
141
+ toMatchSchema: jsonSchemaMatchers.toMatchSchema,
142
+ toMatchSignedCookie,
143
+ toMatchUuid4,
144
+ toMatchUuid5,
145
+ toMatchUuid,
146
+ toThrowBadRequestError,
147
+ toThrowConfigurationError,
148
+ toThrowForbiddenError,
149
+ toThrowInternalError,
150
+ toThrowJaypieError,
151
+ toThrowNotFoundError,
152
+ toThrowUnauthorizedError,
153
+ };
154
+ ```
155
+
120
156
  testSetup.js
121
157
 
122
158
  ```javascript
@@ -174,6 +210,45 @@ expect(json).not.toMatchSchema(jsonApiSchema);
174
210
 
175
211
  From `jest-json-schema`; see [README](https://github.com/americanexpress/jest-json-schema?tab=readme-ov-file#tomatchschemaschema)
176
212
 
213
+
214
+ #### `expect(subject).toMatch*()` Regular Expression Matchers
215
+
216
+ Note: these regular expressions matchers so not verify the value is value, only that it matches the pattern (it "looks like" something). For example, `expect("123e4567-e89b-12d3-a456-426614174000").toMatchUuid()` will pass because the string matches a UUID pattern, even though it is not a valid UUID.
217
+
218
+ * `toMatchBase64`
219
+ * `toMatchJwt`
220
+ * `toMatchMongoId`
221
+ * `toMatchSignedCookie`
222
+ * `toMatchUuid4`
223
+ * `toMatchUuid5`
224
+ * `toMatchUuid`
225
+
226
+ #### `expect(subject).toThrowJaypieError()`
227
+
228
+ ```javascript
229
+ import { ConfigurationError } from "@jaypie/core";
230
+
231
+ const error = new ConfigurationError();
232
+ expect(() => {
233
+ throw error;
234
+ }).toThrowJaypieError();
235
+ ```
236
+
237
+ Do not forget to `await expect` when passing `async` functions:
238
+
239
+ ```javascript
240
+ import { ConfigurationError } from "@jaypie/core";
241
+
242
+ const error = new ConfigurationError();
243
+ await expect(async () => {
244
+ throw error;
245
+ }).toThrowJaypieError();
246
+
247
+ // Breaks and causes a false-positive because `expect` did not `await`
248
+ // expect(async () => {}).toThrowJaypieError();
249
+ // > Error: Expected function to throw a JaypieError, but it did not throw.
250
+ ```
251
+
177
252
  ### `mockLogFactory()`
178
253
 
179
254
  Creates a mock of the `log` provided by `@jaypie/core`.
@@ -193,7 +268,7 @@ Restores the `log` provided by `@jaypie/core`, commonly performed `afterEach` wi
193
268
 
194
269
  ### `spyLog(log)`
195
270
 
196
- Spies on the `log` provided by `@jaypie/core`, commonly performed `beforeEach` with `restoreLog` in `afterEach`.
271
+ Spies on the `log` provided by `@jaypie/core`, commonly performed `beforeEach` with `restoreLog` in `afterEach`. Not necessary when mocking the entire Jaypie module.
197
272
 
198
273
  ```javascript
199
274
  import { restoreLog, spyLog } from "@jaypie/testkit";
@@ -234,13 +309,13 @@ const event = sqsTestRecords(
234
309
  * matcher toBeJaypieData
235
310
  * matcher toBeJaypieDataObject
236
311
  * matcher toBeJaypieDataArray
237
- * matcher toThrowJaypieError
238
312
  * ...@knowdev/jest
239
313
 
240
314
  ## 📝 Changelog
241
315
 
242
316
  | Date | Version | Summary |
243
317
  | ---------- | ------- | -------------- |
318
+ | 7/16/2024 | 1.0.21 | Export Jaypie mock as default |
244
319
  | 3/20/2024 | 1.0.2 | Export `LOG` |
245
320
  | 3/16/2024 | 1.0.0 | Artists ship |
246
321
  | 3/15/2024 | 0.1.0 | Initial deploy |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaypie/testkit",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "author": "Finlayson Studio",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -8,11 +8,20 @@
8
8
  "format": "npm run format:package && npm run format:lint",
9
9
  "format:lint": "eslint --fix .",
10
10
  "format:package": "sort-package-json ./package.json",
11
+ "init:commander": "hygen jaypie commander-init",
11
12
  "init:deploy": "hygen jaypie workflow-npm",
12
13
  "lint": "eslint .",
13
14
  "new": "hygen jaypie vite",
15
+ "new:command": "hygen jaypie command",
16
+ "new:constants": "hygen jaypie constants",
17
+ "new:hygen": "hygen jaypie hygen",
18
+ "new:index": "hygen jaypie index",
19
+ "new:model": "hygen jaypie model",
20
+ "new:test": "hygen jaypie vitest",
14
21
  "test": "vitest",
22
+ "test:spec:constants": "vitest run ./src/__tests__/constants.spec.js",
15
23
  "test:spec:index": "vitest run ./src/__tests__/index.spec.js",
24
+ "test:spec:jaypie.mock": "vitest run ./src/__tests__/jaypie.mock.spec.js",
16
25
  "test:spec:jsonApiSchema.module": "vitest run ./src/__tests__/jsonApiSchema.module.spec.js",
17
26
  "test:spec:matchers.module": "vitest run ./src/__tests__/matchers.module.spec.js",
18
27
  "test:spec:mockLog.module": "vitest run ./src/__tests__/mockLog.module.spec.js",
@@ -20,20 +29,19 @@
20
29
  "test:spec:toBeCalledWithInitialParams.matcher": "vitest run ./src/matchers/__tests__/toBeCalledWithInitialParams.matcher.spec.js",
21
30
  "test:spec:toBeClass.matcher": "vitest run ./src/matchers/__tests__/toBeClass.matcher.spec.js",
22
31
  "test:spec:toBeJaypieError.matcher": "vitest run ./src/matchers/__tests__/toBeJaypieError.matcher.spec.js",
32
+ "test:spec:toMatch.matcher": "vitest run ./src/matchers/__tests__/toMatch.matcher.spec.js",
23
33
  "test:spec:toThrowJaypieError.matcher": "vitest run ./src/matchers/__tests__/toThrowJaypieError.matcher.spec.js"
24
34
  },
25
35
  "dependencies": {
26
36
  "@jaypie/core": "^1.0.33",
37
+ "jaypie": "^1.0.44",
27
38
  "jest-json-schema": "^6.1.0",
28
39
  "lodash.isequal": "^4.5.0",
29
40
  "uuid": "^9.0.1"
30
41
  },
31
42
  "devDependencies": {
32
43
  "eslint": "^8.57.0",
33
- "eslint-config-prettier": "^9.1.0",
34
- "eslint-plugin-import": "^2.29.1",
35
- "eslint-plugin-prettier": "^5.1.3",
36
- "eslint-plugin-vitest": "^0.3.26",
44
+ "eslint-config-jaypie": "^1.0.7",
37
45
  "hygen": "^6.2.11",
38
46
  "jest-extended": "^4.0.2",
39
47
  "prettier": "^3.2.5",
@@ -0,0 +1,28 @@
1
+ //
2
+ //
3
+ // Constants
4
+ //
5
+
6
+ export const LOG = {
7
+ LEVEL: {
8
+ ALL: "all",
9
+ DEBUG: "debug",
10
+ ERROR: "error",
11
+ FATAL: "fatal",
12
+ INFO: "info",
13
+ SILENT: "silent",
14
+ TRACE: "trace",
15
+ WARN: "warn",
16
+ },
17
+ };
18
+
19
+ export const RE_BASE64_PATTERN = /^[a-zA-Z0-9\\+\\/]+$/;
20
+ export const RE_JWT_PATTERN = /^([^.]+)\.([^.]+)\.([^.]+)$/;
21
+ export const RE_MONGO_ID_PATTERN = /^[a-f0-9]{24}$/i;
22
+ export const RE_SIGNED_COOKIE_PATTERN = /^s:([^.]+)\.([^.]+)$/;
23
+ export const RE_UUID_4_PATTERN =
24
+ /^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}$/i;
25
+ export const RE_UUID_5_PATTERN =
26
+ /^[a-f0-9]{8}-?[a-f0-9]{4}-?5[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}$/i;
27
+ export const RE_UUID_PATTERN =
28
+ /^[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}$/i;
package/src/index.js CHANGED
@@ -1,20 +1,11 @@
1
+ import * as mockingJay from "./jaypie.mock.js";
2
+
1
3
  //
2
4
  //
3
5
  // Constants
4
6
  //
5
7
 
6
- export const LOG = {
7
- LEVEL: {
8
- ALL: "all",
9
- DEBUG: "debug",
10
- ERROR: "error",
11
- FATAL: "fatal",
12
- INFO: "info",
13
- SILENT: "silent",
14
- TRACE: "trace",
15
- WARN: "warn",
16
- },
17
- };
8
+ export { LOG } from "./constants.js";
18
9
 
19
10
  //
20
11
  //
@@ -25,3 +16,5 @@ export { jsonApiErrorSchema, jsonApiSchema } from "./jsonApiSchema.module.js";
25
16
  export { default as matchers } from "./matchers.module.js";
26
17
  export { mockLogFactory, restoreLog, spyLog } from "./mockLog.module.js";
27
18
  export { default as sqsTestRecords } from "./sqsTestRecords.function.js";
19
+
20
+ export default mockingJay;
@@ -0,0 +1,206 @@
1
+ import { getMessages as originalGetMessages } from "@jaypie/aws";
2
+ import { force, uuid as originalUuid } from "@jaypie/core";
3
+ import { BadRequestError, JAYPIE, log, UnavailableError } from "@jaypie/core";
4
+ import { beforeAll, vi } from "vitest";
5
+
6
+ import { spyLog } from "./mockLog.module.js";
7
+
8
+ //
9
+ //
10
+ // Setup
11
+ //
12
+
13
+ const TAG = JAYPIE.LIB.TESTKIT;
14
+
15
+ // Export all the modules from Jaypie packages:
16
+
17
+ export * from "@jaypie/aws";
18
+ export * from "@jaypie/core";
19
+ export * from "@jaypie/express";
20
+ export * from "@jaypie/datadog";
21
+ export * from "@jaypie/lambda";
22
+ export * from "@jaypie/mongoose";
23
+
24
+ // Spy on log:
25
+
26
+ beforeAll(() => {
27
+ spyLog(log);
28
+ });
29
+
30
+ // afterEach(() => {
31
+ // This is not necessary because the log isn't being used outside tests:
32
+ // restoreLog(log);
33
+ // The is the client's responsibility:
34
+ // vi.clearAllMocks();
35
+ // });
36
+
37
+ //
38
+ //
39
+ // Mock Functions
40
+ //
41
+
42
+ // @jaypie/aws
43
+
44
+ export const getMessages = vi.fn((...params) => originalGetMessages(...params));
45
+
46
+ export const getSecret = vi.fn(() => {
47
+ return `_MOCK_SECRET_[${TAG}]`;
48
+ });
49
+
50
+ export const sendBatchMessages = vi.fn(() => {
51
+ // TODO: better default value
52
+ return { value: `_MOCK_BATCH_MESSAGES_[${TAG}]` };
53
+ });
54
+
55
+ export const sendMessage = vi.fn(() => {
56
+ // TODO: better default value
57
+ return { value: `_MOCK_MESSAGE_[${TAG}]` };
58
+ });
59
+
60
+ // @jaypie/core Functions
61
+
62
+ export const envBoolean = vi.fn(() => {
63
+ return true;
64
+ });
65
+
66
+ export const jaypieHandler = vi.fn(
67
+ (
68
+ handler,
69
+ {
70
+ setup = [],
71
+ teardown = [],
72
+ unavailable = force.boolean(process.env.PROJECT_UNAVAILABLE),
73
+ validate = [],
74
+ } = {},
75
+ ) => {
76
+ return async (...args) => {
77
+ let result;
78
+ let thrownError;
79
+ if (unavailable) throw new UnavailableError();
80
+ for (const validator of validate) {
81
+ if (typeof validator === "function") {
82
+ const valid = await validator(...args);
83
+ if (valid === false) {
84
+ throw new BadRequestError();
85
+ }
86
+ }
87
+ }
88
+ try {
89
+ for (const setupFunction of setup) {
90
+ if (typeof setupFunction === "function") {
91
+ await setupFunction(...args);
92
+ }
93
+ }
94
+ result = handler(...args);
95
+ } catch (error) {
96
+ thrownError = error;
97
+ }
98
+ for (const teardownFunction of teardown) {
99
+ if (typeof teardownFunction === "function") {
100
+ try {
101
+ await teardownFunction(...args);
102
+ } catch (error) {
103
+ // eslint-disable-next-line no-console
104
+ console.error(error);
105
+ }
106
+ }
107
+ }
108
+ if (thrownError) {
109
+ throw thrownError;
110
+ }
111
+ return result;
112
+ };
113
+ },
114
+ );
115
+
116
+ export const sleep = vi.fn(() => {
117
+ return true;
118
+ });
119
+
120
+ export const uuid = vi.fn(originalUuid);
121
+
122
+ // @jaypie/datadog
123
+
124
+ export const submitMetric = vi.fn(() => {
125
+ return true;
126
+ });
127
+
128
+ export const submitMetricSet = vi.fn(() => {
129
+ return true;
130
+ });
131
+
132
+ // @jaypie/express
133
+
134
+ export const expressHandler = vi.fn((handler, props = {}) => {
135
+ if (typeof handler !== "function") {
136
+ throw new BadRequestError("handler must be a function");
137
+ }
138
+ if (!props) {
139
+ props = {};
140
+ }
141
+ props.setup = force.array(props.setup); // allows a single item
142
+ props.teardown = force.array(props.teardown); // allows a single item
143
+ if (!Array.isArray(props.setup)) {
144
+ props.setup = [];
145
+ }
146
+ if (props.locals === null) {
147
+ throw new BadRequestError("locals cannot be null");
148
+ }
149
+ if (props.locals) {
150
+ if (typeof props.locals !== "object" || Array.isArray(props.locals)) {
151
+ throw new BadRequestError("locals must be an object");
152
+ }
153
+ // Locals
154
+ const keys = Object.keys(props.locals);
155
+ if (keys.length > 0) {
156
+ props.setup.push((req = {}) => {
157
+ if (typeof req !== "object") {
158
+ throw new BadRequestError("req must be an object");
159
+ }
160
+ // Set req.locals if it doesn't exist
161
+ if (!req.locals) req.locals = {};
162
+ if (typeof req.locals !== "object" || Array.isArray(req.locals)) {
163
+ throw new BadRequestError("req.locals must be an object");
164
+ }
165
+ if (!req.locals._jaypie) req.locals._jaypie = {};
166
+ });
167
+ const localsSetup = async (localsReq, localsRes) => {
168
+ for (let i = 0; i < keys.length; i += 1) {
169
+ const key = keys[i];
170
+ if (typeof props.locals[key] === "function") {
171
+ // eslint-disable-next-line no-await-in-loop
172
+ localsReq.locals[key] = await props.locals[key](
173
+ localsReq,
174
+ localsRes,
175
+ );
176
+ } else {
177
+ localsReq.locals[key] = props.locals[key];
178
+ }
179
+ }
180
+ };
181
+ props.setup.push(localsSetup);
182
+ }
183
+ }
184
+ return jaypieHandler(handler, props);
185
+ });
186
+
187
+ // @jaypie/lambda
188
+
189
+ // For testing, this is the same as the jaypieHandler
190
+ export const lambdaHandler = vi.fn((handler, props = {}) => {
191
+ return jaypieHandler(handler, props);
192
+ });
193
+
194
+ // @jaypie/mongoose
195
+
196
+ export const connect = vi.fn(() => {
197
+ return true;
198
+ });
199
+
200
+ export const connectFromSecretEnv = vi.fn(() => {
201
+ return true;
202
+ });
203
+
204
+ export const disconnect = vi.fn(() => {
205
+ return true;
206
+ });
@@ -0,0 +1,76 @@
1
+ import {
2
+ RE_BASE64_PATTERN,
3
+ RE_JWT_PATTERN,
4
+ RE_MONGO_ID_PATTERN,
5
+ RE_SIGNED_COOKIE_PATTERN,
6
+ RE_UUID_4_PATTERN,
7
+ RE_UUID_5_PATTERN,
8
+ RE_UUID_PATTERN,
9
+ } from "../constants.js";
10
+
11
+ //
12
+ //
13
+ // Helper
14
+ //
15
+
16
+ function forSubjectToMatchPattern(
17
+ subject,
18
+ pattern,
19
+ { patternName = "pattern" } = {},
20
+ ) {
21
+ if (pattern.test(subject)) {
22
+ return {
23
+ message: () => `expected "${subject}" not to match ${patternName}`,
24
+ pass: true,
25
+ };
26
+ }
27
+ return {
28
+ message: () => `expected "${subject}" to match ${patternName}`,
29
+ pass: false,
30
+ };
31
+ }
32
+
33
+ //
34
+ //
35
+ // Main
36
+ //
37
+
38
+ export const toMatchBase64 = (subject) =>
39
+ forSubjectToMatchPattern(subject, RE_BASE64_PATTERN, {
40
+ patternName: "Base64",
41
+ });
42
+
43
+ export const toMatchJwt = (subject) =>
44
+ forSubjectToMatchPattern(subject, RE_JWT_PATTERN, {
45
+ patternName: "JWT",
46
+ });
47
+
48
+ export const toMatchMongoId = (subject) =>
49
+ forSubjectToMatchPattern(subject, RE_MONGO_ID_PATTERN, {
50
+ patternName: "MongoDbId",
51
+ });
52
+
53
+ export const toMatchSignedCookie = (subject) =>
54
+ forSubjectToMatchPattern(subject, RE_SIGNED_COOKIE_PATTERN, {
55
+ patternName: "Signed-Cookie",
56
+ });
57
+
58
+ export const toMatchUuid4 = (subject) =>
59
+ forSubjectToMatchPattern(subject, RE_UUID_4_PATTERN, {
60
+ patternName: "UUIDv4",
61
+ });
62
+
63
+ export const toMatchUuid5 = (subject) =>
64
+ forSubjectToMatchPattern(subject, RE_UUID_5_PATTERN, {
65
+ patternName: "UUIDv5",
66
+ });
67
+
68
+ /**
69
+ * Determines if subject matches a UUID pattern.
70
+ * Does _NOT_ check if the UUID is valid.
71
+ * @param {String} subject
72
+ */
73
+ export const toMatchUuid = (subject) =>
74
+ forSubjectToMatchPattern(subject, RE_UUID_PATTERN, {
75
+ patternName: "UUID",
76
+ });
@@ -12,6 +12,16 @@ import toThrowJaypieError, {
12
12
  toThrowUnauthorizedError,
13
13
  } from "./matchers/toThrowJaypieError.matcher.js";
14
14
 
15
+ import {
16
+ toMatchBase64,
17
+ toMatchJwt,
18
+ toMatchMongoId,
19
+ toMatchSignedCookie,
20
+ toMatchUuid4,
21
+ toMatchUuid5,
22
+ toMatchUuid,
23
+ } from "./matchers/toMatch.matcher.js";
24
+
15
25
  //
16
26
  //
17
27
  // Export
@@ -22,7 +32,14 @@ export default {
22
32
  toBeClass,
23
33
  toBeJaypieError,
24
34
  toBeValidSchema: jsonSchemaMatchers.toBeValidSchema,
35
+ toMatchBase64,
36
+ toMatchJwt,
37
+ toMatchMongoId,
25
38
  toMatchSchema: jsonSchemaMatchers.toMatchSchema,
39
+ toMatchSignedCookie,
40
+ toMatchUuid4,
41
+ toMatchUuid5,
42
+ toMatchUuid,
26
43
  toThrowBadRequestError,
27
44
  toThrowConfigurationError,
28
45
  toThrowForbiddenError,
@@ -7,6 +7,7 @@ export function mockLogFactory() {
7
7
  error: vi.fn(),
8
8
  fatal: vi.fn(),
9
9
  info: vi.fn(),
10
+ init: vi.fn(),
10
11
  lib: vi.fn(),
11
12
  tag: vi.fn(),
12
13
  trace: vi.fn(),
@@ -23,6 +24,7 @@ export function mockLogFactory() {
23
24
  mock.trace.var = mock.var;
24
25
  mock.warn.var = mock.var;
25
26
  // Have modules return correct objects
27
+ mock.init.mockReturnValue(null);
26
28
  mock.lib.mockReturnValue(mock);
27
29
  mock.with.mockReturnValue(mock);
28
30
 
@@ -32,6 +34,7 @@ export function mockLogFactory() {
32
34
  error: mock.error,
33
35
  fatal: mock.fatal,
34
36
  info: mock.info,
37
+ init: mock.init,
35
38
  lib: mock.lib,
36
39
  tag: mock.tag,
37
40
  trace: mock.trace,
@@ -55,6 +58,7 @@ const logMethodNames = [
55
58
  "error",
56
59
  "fatal",
57
60
  "info",
61
+ "init",
58
62
  "lib",
59
63
  "tag",
60
64
  "trace",