@memberjunction/unit-testing 0.0.1 → 4.3.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/README.md CHANGED
@@ -1,45 +1,332 @@
1
1
  # @memberjunction/unit-testing
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Utilities and mocks for writing unit tests in the MemberJunction monorepo using Vitest.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ## Installation
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ ```bash
8
+ npm install --save-dev @memberjunction/unit-testing
9
+ ```
8
10
 
9
- ## Purpose
11
+ This package is typically already included as a dev dependency in MemberJunction projects.
10
12
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/unit-testing`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
13
+ ## Overview
15
14
 
16
- ## What is OIDC Trusted Publishing?
15
+ This package provides helper functions and mock utilities to simplify unit testing of MemberJunction components. It handles common testing challenges like:
17
16
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
17
+ - **Singleton reset** - Clean state between tests
18
+ - **Entity mocking** - Mock BaseEntity behavior without database
19
+ - **RunView mocking** - Mock data loading operations
20
+ - **Custom matchers** - Additional Vitest assertions
19
21
 
20
- ## Setup Instructions
22
+ ## Utilities
21
23
 
22
- To properly configure OIDC trusted publishing for this package:
24
+ ### Singleton Reset
23
25
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
26
+ MemberJunction uses singletons for engines and global state. Reset them between tests to ensure isolation.
28
27
 
29
- ## DO NOT USE THIS PACKAGE
28
+ #### `resetMJSingletons()`
30
29
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
30
+ Clears ALL MJ singleton instances from the global store.
36
31
 
37
- ## More Information
32
+ ```typescript
33
+ import { describe, it, beforeEach } from 'vitest';
34
+ import { resetMJSingletons } from '@memberjunction/unit-testing';
38
35
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
36
+ describe('MyEngine', () => {
37
+ beforeEach(() => {
38
+ resetMJSingletons(); // Clean slate for each test
39
+ });
42
40
 
43
- ---
41
+ it('should create fresh engine instance', () => {
42
+ const engine = MyEngine.Instance; // Gets new instance
43
+ // ... test
44
+ });
45
+ });
46
+ ```
44
47
 
45
- **Maintained for OIDC setup purposes only**
48
+ #### `resetClassFactory()`
49
+
50
+ Resets only the ClassFactory registrations. Lighter weight than `resetMJSingletons`.
51
+
52
+ ```typescript
53
+ import { resetClassFactory } from '@memberjunction/unit-testing';
54
+
55
+ beforeEach(() => {
56
+ resetClassFactory(); // Only reset class registrations
57
+ });
58
+ ```
59
+
60
+ #### `resetObjectCache()`
61
+
62
+ Clears the global object cache used by MJ for caching data.
63
+
64
+ ```typescript
65
+ import { resetObjectCache } from '@memberjunction/unit-testing';
66
+
67
+ beforeEach(() => {
68
+ resetObjectCache(); // Clear cached objects
69
+ });
70
+ ```
71
+
72
+ ### Entity Mocking
73
+
74
+ #### `createMockEntity<T>(data, options?)`
75
+
76
+ Creates a Proxy-based mock that behaves like a BaseEntity with getter/setter properties.
77
+
78
+ **Why needed:** BaseEntity uses getters/setters, so the spread operator (`...entity`) doesn't work. This mock provides the same interface without requiring a real database.
79
+
80
+ ```typescript
81
+ import { createMockEntity } from '@memberjunction/unit-testing';
82
+
83
+ // Create a mock user entity
84
+ const mockUser = createMockEntity({
85
+ ID: 'user-123',
86
+ Name: 'Test User',
87
+ Email: 'test@example.com',
88
+ Status: 'Active'
89
+ });
90
+
91
+ // Use like a real entity
92
+ console.log(mockUser.Name); // 'Test User'
93
+ mockUser.Status = 'Inactive'; // Setter works
94
+ console.log(mockUser.Get('Email')); // 'test@example.com'
95
+ console.log(mockUser.GetAll()); // { ID: '...', Name: '...', ... }
96
+
97
+ await mockUser.Save(); // Mock save (always succeeds)
98
+ console.log(mockUser.Dirty); // false after save
99
+ ```
100
+
101
+ **Options:**
102
+
103
+ ```typescript
104
+ interface MockEntityOptions {
105
+ isSaved?: boolean; // Default: true - Entity appears saved to DB
106
+ isDirty?: boolean; // Default: false - Entity appears clean
107
+ }
108
+
109
+ // Create unsaved entity
110
+ const newEntity = createMockEntity(
111
+ { ID: '', Name: 'New User' },
112
+ { isSaved: false, isDirty: true }
113
+ );
114
+ ```
115
+
116
+ **Mock Entity Methods:**
117
+
118
+ - `Get(fieldName)` - Get field value (case-insensitive)
119
+ - `Set(fieldName, value)` - Set field value (marks dirty)
120
+ - `GetAll()` - Returns all fields as plain object
121
+ - `Save()` - Mock save operation (always succeeds, clears dirty flag)
122
+ - `Delete()` - Mock delete operation (always succeeds)
123
+ - `Dirty` - Boolean indicating if entity has unsaved changes
124
+ - `IsSaved` - Boolean indicating if entity exists in DB
125
+ - `PrimaryKey` - Mock primary key object
126
+
127
+ ### RunView Mocking
128
+
129
+ #### `mockRunView(entityName, mockResults)`
130
+
131
+ Mocks a single `RunView` operation to return specific results.
132
+
133
+ ```typescript
134
+ import { mockRunView } from '@memberjunction/unit-testing';
135
+ import { vi } from 'vitest';
136
+
137
+ // Mock RunView for 'Users' entity
138
+ const mockUsers = [
139
+ createMockEntity({ ID: '1', Name: 'Alice' }),
140
+ createMockEntity({ ID: '2', Name: 'Bob' })
141
+ ];
142
+
143
+ const runViewSpy = mockRunView('Users', mockUsers);
144
+
145
+ // Now when code calls RunView:
146
+ const rv = new RunView();
147
+ const result = await rv.RunView({ EntityName: 'Users' });
148
+ // result.Results === mockUsers
149
+
150
+ // Verify it was called
151
+ expect(runViewSpy).toHaveBeenCalledWith(
152
+ expect.objectContaining({ EntityName: 'Users' })
153
+ );
154
+ ```
155
+
156
+ #### `mockRunViews(mocks)`
157
+
158
+ Mocks multiple `RunView` operations at once.
159
+
160
+ ```typescript
161
+ import { mockRunViews } from '@memberjunction/unit-testing';
162
+
163
+ mockRunViews({
164
+ 'Users': mockUsers,
165
+ 'Actions': mockActions,
166
+ 'AI Models': mockModels
167
+ });
168
+
169
+ // All three entities will return mock data
170
+ ```
171
+
172
+ #### `resetRunViewMocks()`
173
+
174
+ Clears all RunView mocks.
175
+
176
+ ```typescript
177
+ import { resetRunViewMocks } from '@memberjunction/unit-testing';
178
+
179
+ afterEach(() => {
180
+ resetRunViewMocks(); // Clean up mocks
181
+ });
182
+ ```
183
+
184
+ ### Custom Matchers
185
+
186
+ #### `installCustomMatchers()`
187
+
188
+ Installs additional Vitest matchers for MemberJunction-specific assertions.
189
+
190
+ ```typescript
191
+ import { installCustomMatchers } from '@memberjunction/unit-testing';
192
+ import { beforeAll } from 'vitest';
193
+
194
+ beforeAll(() => {
195
+ installCustomMatchers();
196
+ });
197
+
198
+ // Now use custom matchers in your tests
199
+ // (See vitest.d.ts for available custom matchers)
200
+ ```
201
+
202
+ ## Common Patterns
203
+
204
+ ### Test Structure with Full Setup
205
+
206
+ ```typescript
207
+ import { describe, it, expect, beforeEach } from 'vitest';
208
+ import {
209
+ resetMJSingletons,
210
+ createMockEntity,
211
+ mockRunView
212
+ } from '@memberjunction/unit-testing';
213
+
214
+ describe('MyService', () => {
215
+ beforeEach(() => {
216
+ // Reset singletons for clean state
217
+ resetMJSingletons();
218
+ });
219
+
220
+ it('should process users correctly', async () => {
221
+ // Setup mock data
222
+ const mockUsers = [
223
+ createMockEntity({ ID: '1', Name: 'Alice', Status: 'Active' }),
224
+ createMockEntity({ ID: '2', Name: 'Bob', Status: 'Inactive' })
225
+ ];
226
+
227
+ // Mock RunView to return mock data
228
+ mockRunView('Users', mockUsers);
229
+
230
+ // Test your service
231
+ const service = new MyService();
232
+ const result = await service.getActiveUsers();
233
+
234
+ // Assertions
235
+ expect(result).toHaveLength(1);
236
+ expect(result[0].Name).toBe('Alice');
237
+ });
238
+ });
239
+ ```
240
+
241
+ ### Mocking Entity Creation
242
+
243
+ When testing code that creates entities via `Metadata.GetEntityObject()`:
244
+
245
+ ```typescript
246
+ import { vi } from 'vitest';
247
+ import { createMockEntity } from '@memberjunction/unit-testing';
248
+
249
+ // Mock the Metadata class
250
+ vi.mock('@memberjunction/core', () => ({
251
+ Metadata: vi.fn(function() {
252
+ return {
253
+ GetEntityObject: vi.fn(async (entityName) => {
254
+ return createMockEntity({ ID: '', Name: '' }, { isSaved: false });
255
+ })
256
+ };
257
+ })
258
+ }));
259
+ ```
260
+
261
+ ### Testing Singleton Engines
262
+
263
+ ```typescript
264
+ import { resetMJSingletons } from '@memberjunction/unit-testing';
265
+
266
+ describe('ActionEngine', () => {
267
+ beforeEach(() => {
268
+ resetMJSingletons(); // Ensures fresh instance
269
+ });
270
+
271
+ it('should load actions', async () => {
272
+ const engine = ActionEngine.Instance;
273
+ await engine.Load();
274
+ expect(engine.Actions.length).toBeGreaterThan(0);
275
+ });
276
+
277
+ it('should get separate instance after reset', async () => {
278
+ const engine1 = ActionEngine.Instance;
279
+ resetMJSingletons();
280
+ const engine2 = ActionEngine.Instance;
281
+
282
+ expect(engine1).not.toBe(engine2); // Different instances
283
+ });
284
+ });
285
+ ```
286
+
287
+ ## TypeScript Support
288
+
289
+ This package includes TypeScript definitions for all utilities. Import types as needed:
290
+
291
+ ```typescript
292
+ import type { MockEntityOptions, MockEntityMethods } from '@memberjunction/unit-testing';
293
+ ```
294
+
295
+ ## Best Practices
296
+
297
+ ### DO:
298
+ - ✅ Always reset singletons in `beforeEach()` for test isolation
299
+ - ✅ Use `createMockEntity()` instead of plain objects for BaseEntity mocks
300
+ - ✅ Use `mockRunView()` to avoid database dependencies in unit tests
301
+ - ✅ Keep mocks simple and focused on the test case
302
+
303
+ ### DON'T:
304
+ - ❌ Share mock data between tests (creates hidden dependencies)
305
+ - ❌ Forget to reset singletons (causes test interference)
306
+ - ❌ Over-mock (test real logic where possible)
307
+ - ❌ Use real database connections in unit tests (use integration tests instead)
308
+
309
+ ## Related Documentation
310
+
311
+ - **Testing Strategy**: See `/unit-testing/README.md` for comprehensive testing guidelines
312
+ - **Analytics**: See `/unit-testing/README.md` for test reporting and analytics
313
+ - **Integration Tests**: See `/packages/TestingFramework/` for full-stack testing
314
+
315
+ ## Examples
316
+
317
+ Look at existing tests for usage examples:
318
+ - `/packages/MJCore/src/__tests__/` - Core functionality tests
319
+ - `/packages/Actions/Engine/src/__tests__/` - Action engine tests
320
+ - `/packages/AI/*/src/__tests__/` - AI provider tests
321
+
322
+ ## Contributing
323
+
324
+ When adding new test utilities:
325
+ 1. Add the utility function to appropriate file (`singleton-reset.ts`, `mock-entity.ts`, etc.)
326
+ 2. Export from `index.ts`
327
+ 3. Update this README with usage examples
328
+ 4. Add TypeScript definitions to `vitest.d.ts` if adding custom matchers
329
+
330
+ ## License
331
+
332
+ See repository root LICENSE file.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Install MemberJunction-specific custom matchers for Vitest.
3
+ * Call this once in your setup file or at the top of your test.
4
+ */
5
+ export declare function installCustomMatchers(): void;
6
+ //# sourceMappingURL=custom-matchers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-matchers.d.ts","sourceRoot":"","sources":["../src/custom-matchers.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAmD5C"}
@@ -0,0 +1,53 @@
1
+ import { expect } from 'vitest';
2
+ /**
3
+ * Install MemberJunction-specific custom matchers for Vitest.
4
+ * Call this once in your setup file or at the top of your test.
5
+ */
6
+ export function installCustomMatchers() {
7
+ expect.extend({
8
+ /**
9
+ * Asserts that a value looks like a valid MJ entity (has a non-empty ID)
10
+ */
11
+ toBeValidEntity(received) {
12
+ const entity = received;
13
+ const pass = entity != null &&
14
+ typeof entity === 'object' &&
15
+ 'ID' in entity &&
16
+ typeof entity.ID === 'string' &&
17
+ entity.ID.length > 0;
18
+ return {
19
+ pass,
20
+ message: () => pass
21
+ ? `expected value not to be a valid entity, but it has ID="${entity.ID}"`
22
+ : `expected value to be a valid entity with a non-empty string ID, got ${JSON.stringify(received)}`,
23
+ };
24
+ },
25
+ /**
26
+ * Asserts that a RunView result has Success: true
27
+ */
28
+ toHaveSucceeded(received) {
29
+ const result = received;
30
+ const pass = result != null && result.Success === true;
31
+ return {
32
+ pass,
33
+ message: () => pass
34
+ ? `expected result not to have succeeded`
35
+ : `expected result to have succeeded but got: ${result?.ErrorMessage ?? 'Success was not true'}`,
36
+ };
37
+ },
38
+ /**
39
+ * Asserts that an entity-like object has a specific field
40
+ */
41
+ toHaveEntityField(received, fieldName) {
42
+ const entity = received;
43
+ const pass = entity != null && typeof entity === 'object' && fieldName in entity;
44
+ return {
45
+ pass,
46
+ message: () => pass
47
+ ? `expected entity not to have field "${fieldName}"`
48
+ : `expected entity to have field "${fieldName}", got keys: ${entity ? Object.keys(entity).join(', ') : 'null'}`,
49
+ };
50
+ },
51
+ });
52
+ }
53
+ //# sourceMappingURL=custom-matchers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-matchers.js","sourceRoot":"","sources":["../src/custom-matchers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,CAAC,MAAM,CAAC;QACZ;;WAEG;QACH,eAAe,CAAC,QAAiB;YAC/B,MAAM,MAAM,GAAG,QAA0C,CAAC;YAC1D,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI;gBACzB,OAAO,MAAM,KAAK,QAAQ;gBAC1B,IAAI,IAAI,MAAM;gBACd,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ;gBAC7B,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;YAEvB,OAAO;gBACL,IAAI;gBACJ,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI;oBACjB,CAAC,CAAC,2DAA4D,MAAkC,CAAC,EAAE,GAAG;oBACtG,CAAC,CAAC,uEAAuE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE;aACtG,CAAC;QACJ,CAAC;QAED;;WAEG;QACH,eAAe,CAAC,QAAiB;YAC/B,MAAM,MAAM,GAAG,QAA+D,CAAC;YAC/E,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC;YAEvD,OAAO;gBACL,IAAI;gBACJ,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI;oBACjB,CAAC,CAAC,uCAAuC;oBACzC,CAAC,CAAC,8CAA8C,MAAM,EAAE,YAAY,IAAI,sBAAsB,EAAE;aACnG,CAAC;QACJ,CAAC;QAED;;WAEG;QACH,iBAAiB,CAAC,QAAiB,EAAE,SAAiB;YACpD,MAAM,MAAM,GAAG,QAA0C,CAAC;YAC1D,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,SAAS,IAAI,MAAM,CAAC;YAEjF,OAAO;gBACL,IAAI;gBACJ,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI;oBACjB,CAAC,CAAC,sCAAsC,SAAS,GAAG;oBACpD,CAAC,CAAC,kCAAkC,SAAS,gBAAgB,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE;aAClH,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { resetMJSingletons, resetClassFactory, resetObjectCache } from './singleton-reset.js';
2
+ export { createMockEntity, type MockEntityOptions } from './mock-entity.js';
3
+ export { mockRunView, mockRunViews, resetRunViewMocks } from './mock-run-view.js';
4
+ export { installCustomMatchers } from './custom-matchers.js';
5
+ export type {} from './vitest.d';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { resetMJSingletons, resetClassFactory, resetObjectCache } from './singleton-reset.js';
2
+ export { createMockEntity } from './mock-entity.js';
3
+ export { mockRunView, mockRunViews, resetRunViewMocks } from './mock-run-view.js';
4
+ export { installCustomMatchers } from './custom-matchers.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Options for creating mock entity objects
3
+ */
4
+ export interface MockEntityOptions {
5
+ /** Whether the entity should report as saved (existing in DB) */
6
+ isSaved?: boolean;
7
+ /** Whether the entity should report as dirty */
8
+ isDirty?: boolean;
9
+ }
10
+ /**
11
+ * Creates a mock object that behaves like a BaseEntity with getter/setter properties.
12
+ * Since BaseEntity uses getter/setters, the spread operator doesn't work on real entities.
13
+ * This creates a Proxy-based mock that supports Get(), Set(), GetAll(), and property access.
14
+ *
15
+ * @param data Initial field values for the mock entity
16
+ * @param options Configuration options for the mock's state
17
+ * @returns A proxy object that behaves like a BaseEntity
18
+ */
19
+ export declare function createMockEntity<T extends Record<string, unknown>>(data: T, options?: MockEntityOptions): T & MockEntityMethods;
20
+ export interface MockEntityMethods {
21
+ Get(fieldName: string): unknown;
22
+ Set(fieldName: string, value: unknown): void;
23
+ GetAll(): Record<string, unknown>;
24
+ readonly Dirty: boolean;
25
+ readonly IsSaved: boolean;
26
+ readonly PrimaryKey: {
27
+ KeyValuePairs: Array<{
28
+ FieldName: string;
29
+ Value: unknown;
30
+ }>;
31
+ Values: () => string;
32
+ };
33
+ Save(): Promise<boolean>;
34
+ Delete(): Promise<boolean>;
35
+ }
36
+ //# sourceMappingURL=mock-entity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-entity.d.ts","sourceRoot":"","sources":["../src/mock-entity.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChE,IAAI,EAAE,CAAC,EACP,OAAO,GAAE,iBAAsB,GAC9B,CAAC,GAAG,iBAAiB,CAiEvB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7C,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE;QAAE,aAAa,EAAE,KAAK,CAAC;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,MAAM,CAAA;KAAE,CAAC;IAC3G,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5B"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Creates a mock object that behaves like a BaseEntity with getter/setter properties.
3
+ * Since BaseEntity uses getter/setters, the spread operator doesn't work on real entities.
4
+ * This creates a Proxy-based mock that supports Get(), Set(), GetAll(), and property access.
5
+ *
6
+ * @param data Initial field values for the mock entity
7
+ * @param options Configuration options for the mock's state
8
+ * @returns A proxy object that behaves like a BaseEntity
9
+ */
10
+ export function createMockEntity(data, options = {}) {
11
+ const { isSaved = true, isDirty = false } = options;
12
+ const fields = new Map(Object.entries(data));
13
+ const oldValues = new Map(Object.entries(data));
14
+ let dirty = isDirty;
15
+ const methods = {
16
+ Get(fieldName) {
17
+ const lowerKey = [...fields.keys()].find(k => k.toLowerCase() === fieldName.toLowerCase());
18
+ return lowerKey ? fields.get(lowerKey) : undefined;
19
+ },
20
+ Set(fieldName, value) {
21
+ fields.set(fieldName, value);
22
+ dirty = true;
23
+ },
24
+ GetAll() {
25
+ const result = {};
26
+ for (const [key, value] of fields) {
27
+ result[key] = value;
28
+ }
29
+ return result;
30
+ },
31
+ get Dirty() {
32
+ return dirty;
33
+ },
34
+ get IsSaved() {
35
+ return isSaved;
36
+ },
37
+ get PrimaryKey() {
38
+ return {
39
+ KeyValuePairs: [{ FieldName: 'ID', Value: fields.get('ID') }],
40
+ Values: () => String(fields.get('ID') ?? ''),
41
+ };
42
+ },
43
+ async Save() {
44
+ dirty = false;
45
+ return true;
46
+ },
47
+ async Delete() {
48
+ return true;
49
+ },
50
+ };
51
+ return new Proxy(methods, {
52
+ get(target, prop) {
53
+ // Check methods first
54
+ if (prop in target) {
55
+ const val = target[prop];
56
+ return val;
57
+ }
58
+ // Then check data fields
59
+ const lowerProp = prop.toLowerCase();
60
+ for (const [key, value] of fields) {
61
+ if (key.toLowerCase() === lowerProp) {
62
+ return value;
63
+ }
64
+ }
65
+ return undefined;
66
+ },
67
+ set(_target, prop, value) {
68
+ fields.set(prop, value);
69
+ dirty = true;
70
+ return true;
71
+ },
72
+ });
73
+ }
74
+ //# sourceMappingURL=mock-entity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-entity.js","sourceRoot":"","sources":["../src/mock-entity.ts"],"names":[],"mappings":"AAUA;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAO,EACP,UAA6B,EAAE;IAE/B,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;IACpD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAkB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAkB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,IAAI,KAAK,GAAG,OAAO,CAAC;IAEpB,MAAM,OAAO,GAAsB;QACjC,GAAG,CAAC,SAAiB;YACnB,MAAM,QAAQ,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC;YAC3F,OAAO,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACrD,CAAC;QACD,GAAG,CAAC,SAAiB,EAAE,KAAc;YACnC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YAC7B,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;QACD,MAAM;YACJ,MAAM,MAAM,GAA4B,EAAE,CAAC;YAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;gBAClC,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACtB,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,IAAI,KAAK;YACP,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,OAAO;YACT,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,IAAI,UAAU;YACZ,OAAO;gBACL,aAAa,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7D,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;aAC7C,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI;YACR,KAAK,GAAG,KAAK,CAAC;YACd,OAAO,IAAI,CAAC;QACd,CAAC;QACD,KAAK,CAAC,MAAM;YACV,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC;IAEF,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE;QACxB,GAAG,CAAC,MAAM,EAAE,IAAY;YACtB,sBAAsB;YACtB,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;gBACnB,MAAM,GAAG,GAAG,MAAM,CAAC,IAA+B,CAAC,CAAC;gBACpD,OAAO,GAAG,CAAC;YACb,CAAC;YACD,yBAAyB;YACzB,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;gBAClC,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,SAAS,EAAE,CAAC;oBACpC,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,GAAG,CAAC,OAAO,EAAE,IAAY,EAAE,KAAK;YAC9B,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACxB,KAAK,GAAG,IAAI,CAAC;YACb,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAA0B,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,50 @@
1
+ interface RunViewResult<T = unknown> {
2
+ Success: boolean;
3
+ Results: T[];
4
+ ErrorMessage?: string;
5
+ TotalRowCount?: number;
6
+ RowCount: number;
7
+ Metrics?: Record<string, unknown>;
8
+ }
9
+ type RunViewMockMap = Map<string, unknown[]>;
10
+ /**
11
+ * Configure mock responses for RunView calls.
12
+ * Responses are keyed by entity name (case-insensitive).
13
+ *
14
+ * @param responses Map of entity name to array of result objects
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * mockRunView(new Map([
19
+ * ['Users', [{ ID: '1', Name: 'Test User' }]],
20
+ * ['AI Models', [{ ID: '2', Name: 'GPT-4' }]],
21
+ * ]));
22
+ * ```
23
+ */
24
+ export declare function mockRunView(responses: RunViewMockMap): void;
25
+ /**
26
+ * Create a mock RunView instance that returns configured test data.
27
+ * Use with vi.mock() to replace the real RunView.
28
+ */
29
+ export declare function createMockRunViewClass(): {
30
+ new (): {
31
+ RunView<T = unknown>(params: {
32
+ EntityName?: string;
33
+ ExtraFilter?: string;
34
+ }): Promise<RunViewResult<T>>;
35
+ RunViews<T = unknown>(paramsList: Array<{
36
+ EntityName?: string;
37
+ }>): Promise<Array<RunViewResult<T>>>;
38
+ };
39
+ };
40
+ /**
41
+ * Configure mock responses for batch RunViews calls.
42
+ * Same as mockRunView but semantically indicates batch usage.
43
+ */
44
+ export declare function mockRunViews(responses: RunViewMockMap): void;
45
+ /**
46
+ * Reset all RunView mock responses.
47
+ */
48
+ export declare function resetRunViewMocks(): void;
49
+ export {};
50
+ //# sourceMappingURL=mock-run-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-run-view.d.ts","sourceRoot":"","sources":["../src/mock-run-view.ts"],"names":[],"mappings":"AAEA,UAAU,aAAa,CAAC,CAAC,GAAG,OAAO;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,KAAK,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;AAI7C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAI3D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB;;gBAEpB,CAAC,oBAAoB;YAAE,UAAU,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;iBAW7F,CAAC,wBAAwB,KAAK,CAAC;YAAE,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;;EAQ5G;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAE5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
@@ -0,0 +1,57 @@
1
+ let _mockResponses = new Map();
2
+ /**
3
+ * Configure mock responses for RunView calls.
4
+ * Responses are keyed by entity name (case-insensitive).
5
+ *
6
+ * @param responses Map of entity name to array of result objects
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * mockRunView(new Map([
11
+ * ['Users', [{ ID: '1', Name: 'Test User' }]],
12
+ * ['AI Models', [{ ID: '2', Name: 'GPT-4' }]],
13
+ * ]));
14
+ * ```
15
+ */
16
+ export function mockRunView(responses) {
17
+ _mockResponses = new Map([...responses.entries()].map(([k, v]) => [k.toLowerCase(), v]));
18
+ }
19
+ /**
20
+ * Create a mock RunView instance that returns configured test data.
21
+ * Use with vi.mock() to replace the real RunView.
22
+ */
23
+ export function createMockRunViewClass() {
24
+ return class MockRunView {
25
+ async RunView(params) {
26
+ const entityName = params.EntityName?.toLowerCase() ?? '';
27
+ const results = (_mockResponses.get(entityName) ?? []);
28
+ return {
29
+ Success: true,
30
+ Results: results,
31
+ RowCount: results.length,
32
+ TotalRowCount: results.length,
33
+ };
34
+ }
35
+ async RunViews(paramsList) {
36
+ const results = [];
37
+ for (const params of paramsList) {
38
+ results.push(await this.RunView(params));
39
+ }
40
+ return results;
41
+ }
42
+ };
43
+ }
44
+ /**
45
+ * Configure mock responses for batch RunViews calls.
46
+ * Same as mockRunView but semantically indicates batch usage.
47
+ */
48
+ export function mockRunViews(responses) {
49
+ mockRunView(responses);
50
+ }
51
+ /**
52
+ * Reset all RunView mock responses.
53
+ */
54
+ export function resetRunViewMocks() {
55
+ _mockResponses = new Map();
56
+ }
57
+ //# sourceMappingURL=mock-run-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-run-view.js","sourceRoot":"","sources":["../src/mock-run-view.ts"],"names":[],"mappings":"AAaA,IAAI,cAAc,GAAmB,IAAI,GAAG,EAAE,CAAC;AAE/C;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,WAAW,CAAC,SAAyB;IACnD,cAAc,GAAG,IAAI,GAAG,CACtB,CAAC,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAC/D,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,MAAM,WAAW;QACtB,KAAK,CAAC,OAAO,CAAc,MAAqD;YAC9E,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;YAC1D,MAAM,OAAO,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAQ,CAAC;YAC9D,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,OAAO;gBAChB,QAAQ,EAAE,OAAO,CAAC,MAAM;gBACxB,aAAa,EAAE,OAAO,CAAC,MAAM;aAC9B,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,QAAQ,CAAc,UAA0C;YACpE,MAAM,OAAO,GAA4B,EAAE,CAAC;YAC5C,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;gBAChC,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAI,MAAM,CAAC,CAAC,CAAC;YAC9C,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,SAAyB;IACpD,WAAW,CAAC,SAAS,CAAC,CAAC;AACzB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,cAAc,GAAG,IAAI,GAAG,EAAE,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Reset ALL MJ singletons by clearing their entries from the global object store.
3
+ * Call this in beforeEach() to ensure clean test isolation.
4
+ */
5
+ export declare function resetMJSingletons(): void;
6
+ /**
7
+ * Reset just the ClassFactory registrations without destroying the MJGlobal singleton.
8
+ * Lighter weight than resetMJSingletons - use when you only need a clean ClassFactory.
9
+ */
10
+ export declare function resetClassFactory(): void;
11
+ /**
12
+ * Clear the global ObjectCache.
13
+ */
14
+ export declare function resetObjectCache(): void;
15
+ //# sourceMappingURL=singleton-reset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton-reset.d.ts","sourceRoot":"","sources":["../src/singleton-reset.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CASxC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
@@ -0,0 +1,30 @@
1
+ import { MJGlobal } from '@memberjunction/global';
2
+ import { GetGlobalObjectStore } from '@memberjunction/global';
3
+ /**
4
+ * Reset ALL MJ singletons by clearing their entries from the global object store.
5
+ * Call this in beforeEach() to ensure clean test isolation.
6
+ */
7
+ export function resetMJSingletons() {
8
+ const store = GetGlobalObjectStore();
9
+ if (store) {
10
+ const indexableStore = store;
11
+ const singletonKeys = Object.keys(indexableStore).filter(k => k.startsWith('___SINGLETON__'));
12
+ for (const key of singletonKeys) {
13
+ delete indexableStore[key];
14
+ }
15
+ }
16
+ }
17
+ /**
18
+ * Reset just the ClassFactory registrations without destroying the MJGlobal singleton.
19
+ * Lighter weight than resetMJSingletons - use when you only need a clean ClassFactory.
20
+ */
21
+ export function resetClassFactory() {
22
+ MJGlobal.Instance.Reset();
23
+ }
24
+ /**
25
+ * Clear the global ObjectCache.
26
+ */
27
+ export function resetObjectCache() {
28
+ MJGlobal.Instance.ObjectCache.Clear();
29
+ }
30
+ //# sourceMappingURL=singleton-reset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton-reset.js","sourceRoot":"","sources":["../src/singleton-reset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,KAAK,GAAG,oBAAoB,EAAE,CAAC;IACrC,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,cAAc,GAAG,KAAgC,CAAC;QACxD,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAC9F,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;YAChC,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;AACxC,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,30 @@
1
1
  {
2
2
  "name": "@memberjunction/unit-testing",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @memberjunction/unit-testing",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "type": "module",
4
+ "version": "4.3.0",
5
+ "description": "MemberJunction: Unit testing utilities and mock infrastructure for Vitest",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "/dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc && tsc-alias -f",
13
+ "test": "vitest run --passWithNoTests",
14
+ "test:watch": "vitest"
15
+ },
16
+ "author": "MemberJunction.com",
17
+ "license": "ISC",
18
+ "devDependencies": {
19
+ "ts-node-dev": "^2.0.0",
20
+ "typescript": "^5.9.3",
21
+ "vitest": "^4.0.18"
22
+ },
23
+ "dependencies": {
24
+ "@memberjunction/global": "4.3.0"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/MemberJunction/MJ"
29
+ }
10
30
  }