@digitaldefiance/express-suite-test-utils 1.0.11 → 1.0.12

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Digital Defiance
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@digitaldefiance/express-suite-test-utils",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
+ "homepage": "https://github.com/Digital-Defiance/test-utils",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Digital-Defiance/test-utils.git"
8
+ },
4
9
  "description": "Test utilities for Digital Defiance Express Suite",
5
10
  "main": "src/index.js",
6
11
  "types": "src/index.d.ts",
@@ -46,6 +51,5 @@
46
51
  "devDependencies": {
47
52
  "@types/node": "^22.0.0",
48
53
  "mongoose": "^8.9.3"
49
- },
50
- "type": "commonjs"
51
- }
54
+ }
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './lib/to-throw-type';
2
+ export * from './lib/console';
3
+ export * from './lib/direct-log';
4
+ export * from './lib/localStorage-mock';
5
+ export * from './lib/bson-mock';
6
+ export * from './lib/react-mocks';
7
+ export * from './lib/mongoose-memory';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Mock bson and crypto modules for Jest tests
3
+ */
4
+
5
+ export class ObjectId {
6
+ constructor(public id: string = '000000000000000000000000') {}
7
+ toString(): string {
8
+ return this.id;
9
+ }
10
+ toHexString(): string {
11
+ return this.id;
12
+ }
13
+ }
14
+
15
+ export class Binary {}
16
+ export class Code {}
17
+ export class DBRef {}
18
+ export class Decimal128 {}
19
+ export class Double {}
20
+ export class Int32 {}
21
+ export class Long {}
22
+ export class MaxKey {}
23
+ export class MinKey {}
24
+ export class Timestamp {}
25
+ export class UUID {}
26
+
27
+ // Mock secp256k1 for ethereum crypto
28
+ export const secp256k1 = {
29
+ CURVE: {
30
+ p: BigInt(0),
31
+ n: BigInt(0),
32
+ },
33
+ };
34
+
35
+ // Default export for ethereum-cryptography
36
+ export default { secp256k1 };
@@ -0,0 +1,69 @@
1
+ /* Reusable console mock helpers for e2e tests */
2
+
3
+ export type ConsoleSpies = {
4
+ log: jest.SpyInstance;
5
+ info: jest.SpyInstance;
6
+ warn: jest.SpyInstance;
7
+ error: jest.SpyInstance;
8
+ debug: jest.SpyInstance;
9
+ };
10
+
11
+ export interface WithConsoleOptions {
12
+ mute?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Wrap a test body with console spies that are always restored.
17
+ * By default mutes console output to keep test logs clean.
18
+ */
19
+ export async function withConsoleMocks<T = unknown>(
20
+ options: WithConsoleOptions,
21
+ fn: (spies: ConsoleSpies) => Promise<T> | T,
22
+ ): Promise<T> {
23
+ const mute = options?.mute !== false; // default true
24
+ const noop = () => undefined;
25
+ const spies: ConsoleSpies = {
26
+ log: jest
27
+ .spyOn(console, 'log')
28
+ .mockImplementation(mute ? noop : console.log),
29
+ info: jest
30
+ .spyOn(console, 'info')
31
+ .mockImplementation(mute ? noop : console.info),
32
+ warn: jest
33
+ .spyOn(console, 'warn')
34
+ .mockImplementation(mute ? noop : console.warn),
35
+ error: jest
36
+ .spyOn(console, 'error')
37
+ .mockImplementation(mute ? noop : console.error),
38
+ debug: jest
39
+ .spyOn(console, 'debug')
40
+ .mockImplementation(
41
+ mute
42
+ ? noop
43
+ : (console.debug as ((...args: unknown[]) => void) | undefined) ??
44
+ noop,
45
+ ),
46
+ };
47
+ try {
48
+ return await fn(spies);
49
+ } finally {
50
+ spies.log.mockRestore();
51
+ spies.info.mockRestore();
52
+ spies.warn.mockRestore();
53
+ spies.error.mockRestore();
54
+ spies.debug.mockRestore();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * True if any call to the spy contains all provided substrings, in order-agnostic check.
60
+ */
61
+ export function spyContains(
62
+ spy: jest.SpyInstance,
63
+ ...needles: string[]
64
+ ): boolean {
65
+ return spy.mock.calls.some((args: unknown[]) => {
66
+ const text = args.map((a) => (typeof a === 'string' ? a : '')).join(' ');
67
+ return needles.every((n) => text.includes(n));
68
+ });
69
+ }
@@ -0,0 +1,105 @@
1
+ import * as fs from 'fs';
2
+
3
+ /**
4
+ * Spies returned from withDirectLogMocks
5
+ */
6
+ export interface DirectLogSpies {
7
+ writeSync: jest.Mock;
8
+ }
9
+
10
+ /**
11
+ * Options for withDirectLogMocks
12
+ */
13
+ export interface WithDirectLogOptions {
14
+ /**
15
+ * If false, do not mute the output (let it pass through).
16
+ * By default (true), mutes output.
17
+ */
18
+ mute?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Wrap a test body with fs.writeSync spy for directLog testing.
23
+ * By default mutes output (does nothing on write).
24
+ * The spy will capture calls with file descriptor and buffer.
25
+ *
26
+ * Note: Requires fs module to be mocked at module level with jest.mock('fs')
27
+ */
28
+ export async function withDirectLogMocks<T = unknown>(
29
+ options: WithDirectLogOptions,
30
+ fn: (spies: DirectLogSpies) => Promise<T> | T,
31
+ ): Promise<T> {
32
+ const mute = options?.mute !== false; // default true
33
+
34
+ // Get the mocked writeSync function
35
+ const writeSync = fs.writeSync as unknown as jest.Mock;
36
+
37
+ // Store previous mock implementation if any
38
+ const previousImpl = writeSync.getMockImplementation();
39
+
40
+ // Set implementation based on mute option
41
+ if (mute) {
42
+ writeSync.mockImplementation(() => undefined);
43
+ } else {
44
+ // Pass through - requires original implementation to be available
45
+ writeSync.mockImplementation((...args: any[]) => {
46
+ // In test environment, we can't easily call the real fs.writeSync
47
+ // so we just no-op but track the calls
48
+ return args[1]?.length || 0;
49
+ });
50
+ }
51
+
52
+ const spies: DirectLogSpies = { writeSync };
53
+
54
+ try {
55
+ return await fn(spies);
56
+ } finally {
57
+ // Clear calls and restore previous implementation
58
+ writeSync.mockClear();
59
+ if (previousImpl) {
60
+ writeSync.mockImplementation(previousImpl);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Helper to check if writeSync was called with a specific file descriptor and message.
67
+ * @param spy The writeSync mock
68
+ * @param fd The file descriptor (1 for stdout, 2 for stderr)
69
+ * @param needles Substrings to search for in the buffer content
70
+ */
71
+ export function directLogContains(
72
+ spy: jest.Mock,
73
+ fd: number,
74
+ ...needles: string[]
75
+ ): boolean {
76
+ return spy.mock.calls.some((args: unknown[]) => {
77
+ const [calledFd, buffer] = args;
78
+ if (calledFd !== fd) return false;
79
+
80
+ const text = buffer instanceof Buffer
81
+ ? buffer.toString('utf8')
82
+ : String(buffer);
83
+
84
+ return needles.every((n) => text.includes(n));
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Get all messages written to a specific file descriptor.
90
+ * @param spy The writeSync mock
91
+ * @param fd The file descriptor (1 for stdout, 2 for stderr)
92
+ */
93
+ export function getDirectLogMessages(
94
+ spy: jest.Mock,
95
+ fd: number,
96
+ ): string[] {
97
+ return spy.mock.calls
98
+ .filter((args: unknown[]) => args[0] === fd)
99
+ .map((args: unknown[]) => {
100
+ const buffer = args[1];
101
+ return buffer instanceof Buffer
102
+ ? buffer.toString('utf8')
103
+ : String(buffer);
104
+ });
105
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Simple localStorage mock for Node.js test environment
3
+ */
4
+ export class LocalStorageMock implements Storage {
5
+ private store: Map<string, string> = new Map();
6
+
7
+ get length(): number {
8
+ return this.store.size;
9
+ }
10
+
11
+ clear(): void {
12
+ this.store.clear();
13
+ }
14
+
15
+ getItem(key: string): string | null {
16
+ return this.store.get(key) ?? null;
17
+ }
18
+
19
+ key(index: number): string | null {
20
+ const keys = Array.from(this.store.keys());
21
+ return keys[index] ?? null;
22
+ }
23
+
24
+ removeItem(key: string): void {
25
+ this.store.delete(key);
26
+ }
27
+
28
+ setItem(key: string, value: string): void {
29
+ this.store.set(key, value);
30
+ }
31
+ }
32
+
33
+ // Set up global localStorage mock
34
+ if (typeof globalThis.localStorage === 'undefined') {
35
+ Object.defineProperty(globalThis, 'localStorage', {
36
+ value: new LocalStorageMock(),
37
+ writable: true,
38
+ configurable: true,
39
+ });
40
+ }
@@ -0,0 +1,64 @@
1
+ import mongoose, { Connection } from '@digitaldefiance/mongoose-types';
2
+ import { MongoMemoryServer } from 'mongodb-memory-server';
3
+
4
+ let mongoServer: MongoMemoryServer | undefined;
5
+ let connection: Connection | undefined;
6
+
7
+ /**
8
+ * Connect to in-memory MongoDB for testing
9
+ * @returns Object with both the connection and URI
10
+ */
11
+ export async function connectMemoryDB(): Promise<{
12
+ connection: Connection;
13
+ uri: string;
14
+ }> {
15
+ // If mongoose is connected but we don't have our server, disconnect first
16
+ if (mongoose.connection.readyState !== 0 && !mongoServer) {
17
+ await mongoose.disconnect();
18
+ connection = undefined;
19
+ }
20
+
21
+ // Create new server if needed
22
+ if (!mongoServer) {
23
+ mongoServer = await MongoMemoryServer.create();
24
+ }
25
+
26
+ const uri = mongoServer.getUri();
27
+
28
+ // Connect if not already connected
29
+ if (mongoose.connection.readyState === 0) {
30
+ await mongoose.connect(uri);
31
+ }
32
+
33
+ connection = mongoose.connection;
34
+
35
+ return { connection, uri };
36
+ }
37
+
38
+ /**
39
+ * Drop all collections and disconnect
40
+ */
41
+ export async function disconnectMemoryDB(): Promise<void> {
42
+ if (connection) {
43
+ await connection.dropDatabase();
44
+ await mongoose.disconnect();
45
+ connection = undefined;
46
+ }
47
+
48
+ if (mongoServer) {
49
+ await mongoServer.stop();
50
+ mongoServer = undefined;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Clear all collections without disconnecting
56
+ */
57
+ export async function clearMemoryDB(): Promise<void> {
58
+ if (connection) {
59
+ const collections = connection.collections;
60
+ for (const key in collections) {
61
+ await collections[key].deleteMany({});
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Mock utilities for React component testing
3
+ */
4
+
5
+ /**
6
+ * Mock timezones for testing
7
+ */
8
+ export const mockTimezones = [
9
+ 'America/New_York',
10
+ 'America/Los_Angeles',
11
+ 'Europe/London',
12
+ 'Europe/Paris',
13
+ 'Asia/Tokyo',
14
+ 'UTC',
15
+ ];
16
+
17
+ /**
18
+ * Get initial timezone mock
19
+ */
20
+ export const mockGetInitialTimezone = (): string => 'UTC';
21
+
22
+ /**
23
+ * Mock RegisterForm props
24
+ */
25
+ export const mockRegisterFormProps = {
26
+ timezones: mockTimezones,
27
+ getInitialTimezone: mockGetInitialTimezone,
28
+ onSubmit: jest.fn().mockResolvedValue({ success: true, message: 'Success' }),
29
+ };
@@ -0,0 +1,200 @@
1
+ import { expect } from '@jest/globals';
2
+ import type { MatcherContext } from 'expect';
3
+ import { readFileSync } from 'fs';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ export type ErrorClass<E extends Error> = new (...args: any[]) => E;
7
+
8
+ declare global {
9
+ namespace jest {
10
+ interface Matchers<R> {
11
+ toThrowType<E extends Error>(
12
+ errorType: ErrorClass<E>,
13
+ validator?: (error: E) => void,
14
+ ): R;
15
+ }
16
+ }
17
+ }
18
+
19
+ interface MatcherError extends Error {
20
+ matcherResult?: {
21
+ expected: unknown;
22
+ received: unknown;
23
+ };
24
+ }
25
+
26
+ function isMatcherError(error: unknown): error is MatcherError {
27
+ return error instanceof Error && 'matcherResult' in error;
28
+ }
29
+
30
+ function extractTestInfo(stackTrace: string) {
31
+ const stackLines = stackTrace.split('\n');
32
+ const anonymousLine = stackLines.find((line) =>
33
+ line.includes('Object.<anonymous>'),
34
+ );
35
+ const match = anonymousLine?.match(/\((.+?\.spec\.ts):(\d+):(\d+)\)/);
36
+
37
+ if (!match) {
38
+ return { testHierarchy: ['Unknown Test'], location: '' };
39
+ }
40
+
41
+ const fullTestPath = match[1];
42
+ const lineNumber = parseInt(match[2]);
43
+ const testFile = fullTestPath.split('/').pop() || '';
44
+
45
+ try {
46
+ const fileContent = readFileSync(fullTestPath, 'utf8');
47
+ const lines = fileContent.split('\n');
48
+ const testLineContent = lines[lineNumber - 1];
49
+ const testNameMatch = testLineContent.match(/it\(['"](.+?)['"]/);
50
+ const testName = testNameMatch?.[1];
51
+
52
+ const testHierarchy = ['Test'];
53
+ if (testName) {
54
+ testHierarchy.push(testName);
55
+ }
56
+
57
+ return {
58
+ testHierarchy,
59
+ location: ` (${testFile}:${lineNumber})`,
60
+ };
61
+ } catch {
62
+ return {
63
+ testHierarchy: ['Test'],
64
+ location: ` (${testFile}:${lineNumber})`,
65
+ };
66
+ }
67
+ }
68
+
69
+ export const toThrowType = async function <E extends Error>(
70
+ this: MatcherContext,
71
+ received: (() => unknown | Promise<unknown>) | Promise<unknown>,
72
+ errorType: ErrorClass<E>,
73
+ validator?: (error: E) => void,
74
+ ): Promise<{ pass: boolean; message: () => string }> {
75
+ const matcherName = 'toThrowType';
76
+ const options = {
77
+ isNot: this.isNot,
78
+ promise: this.promise,
79
+ };
80
+
81
+ let error: unknown;
82
+ let pass = false;
83
+
84
+ try {
85
+ if (this.promise) {
86
+ try {
87
+ await received;
88
+ pass = false;
89
+ } catch (e) {
90
+ error = e;
91
+ if (
92
+ error instanceof Error &&
93
+ (error instanceof errorType || error.constructor === errorType)
94
+ ) {
95
+ pass = true;
96
+ if (validator) {
97
+ await validator(error as E);
98
+ }
99
+ }
100
+ }
101
+ } else {
102
+ if (typeof received !== 'function') {
103
+ throw new Error(
104
+ this.utils.matcherHint(matcherName, undefined, undefined, options) +
105
+ '\n\n' +
106
+ 'Received value must be a function',
107
+ );
108
+ }
109
+ try {
110
+ await (received as () => unknown | Promise<unknown>)();
111
+ pass = false;
112
+ } catch (e) {
113
+ error = e;
114
+ if (
115
+ error instanceof Error &&
116
+ (error instanceof errorType || error.constructor === errorType)
117
+ ) {
118
+ pass = true;
119
+ if (validator) {
120
+ await validator(error as E);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ } catch (validatorError) {
126
+ const error =
127
+ validatorError instanceof Error
128
+ ? validatorError
129
+ : new Error(String(validatorError));
130
+ const message = error.message;
131
+ const stack = error.stack || '';
132
+
133
+ let diffString: string;
134
+ if (isMatcherError(error) && error.matcherResult) {
135
+ diffString =
136
+ this.utils.diff(
137
+ error.matcherResult.expected,
138
+ error.matcherResult.received,
139
+ ) || '';
140
+ } else {
141
+ diffString = this.utils.diff('Error to match assertions', message) || '';
142
+ }
143
+
144
+ const { testHierarchy, location } = extractTestInfo(stack);
145
+
146
+ return {
147
+ pass: false,
148
+ message: () =>
149
+ `\n\n${this.utils.RECEIVED_COLOR(
150
+ `● ${testHierarchy.join(' › ')}${location ? ` ${location}` : ''}`,
151
+ )}\n\n` +
152
+ this.utils.matcherHint(matcherName, undefined, undefined, options) +
153
+ '\n\n' +
154
+ diffString +
155
+ '\n\n' +
156
+ (stack
157
+ ? this.utils.RECEIVED_COLOR(stack.split('\n').slice(1).join('\n'))
158
+ : ''),
159
+ };
160
+ }
161
+
162
+ const testHeader =
163
+ error instanceof Error && error.stack
164
+ ? (() => {
165
+ const { testHierarchy, location } = extractTestInfo(error.stack);
166
+ return `\n\n${this.utils.RECEIVED_COLOR(
167
+ `● ${testHierarchy.join(' › ')}${location}`,
168
+ )}\n\n`;
169
+ })()
170
+ : '\n';
171
+
172
+ return {
173
+ pass,
174
+ message: () =>
175
+ testHeader +
176
+ this.utils.matcherHint(matcherName, undefined, undefined, options) +
177
+ '\n\n' +
178
+ (pass
179
+ ? `Expected function not to throw ${this.utils.printExpected(
180
+ errorType.name,
181
+ )}`
182
+ : this.promise
183
+ ? this.utils.matcherErrorMessage(
184
+ this.utils.matcherHint(matcherName, undefined, undefined, options),
185
+ 'Expected promise to reject',
186
+ 'Promise resolved successfully',
187
+ )
188
+ : this.utils.matcherErrorMessage(
189
+ this.utils.matcherHint(matcherName, undefined, undefined, options),
190
+ `Expected function to throw ${this.utils.printExpected(
191
+ errorType.name,
192
+ )}`,
193
+ `Received: ${this.utils.printReceived(
194
+ error instanceof Error ? error.constructor.name : typeof error,
195
+ )}`,
196
+ )),
197
+ };
198
+ };
199
+
200
+ expect.extend({ toThrowType });
@@ -0,0 +1,31 @@
1
+ /// <reference types="jest" />
2
+
3
+ export {};
4
+
5
+ declare global {
6
+ namespace jest {
7
+ interface Matchers<R> {
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ toThrowType<E extends Error, T extends new (...args: any[]) => E>(
10
+ errorType: T,
11
+ validator?: (error: E) => void,
12
+ ): R;
13
+ }
14
+ }
15
+ }
16
+
17
+ declare module 'expect' {
18
+ interface ExpectExtendMap {
19
+ toThrowType: {
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ <E extends Error, T extends new (...args: any[]) => E>(
22
+ this: jest.MatcherContext,
23
+ received: () => unknown,
24
+ errorType: T,
25
+ validator?: (error: E) => void,
26
+ ): jest.CustomMatcherResult;
27
+ };
28
+ }
29
+ }
30
+
31
+