@htlkg/data 0.0.14 → 0.0.16
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 +72 -0
- package/dist/client/index.d.ts +123 -30
- package/dist/client/index.js +75 -1
- package/dist/client/index.js.map +1 -1
- package/dist/hooks/index.d.ts +76 -2
- package/dist/hooks/index.js +224 -6
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +550 -7
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.d.ts +149 -5
- package/dist/mutations/index.js +397 -0
- package/dist/mutations/index.js.map +1 -1
- package/dist/productInstances-CzT3NZKU.d.ts +98 -0
- package/dist/queries/index.d.ts +54 -2
- package/dist/queries/index.js +60 -1
- package/dist/queries/index.js.map +1 -1
- package/dist/server/index.d.ts +47 -0
- package/dist/server/index.js +59 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +5 -1
- package/src/client/index.ts +82 -3
- package/src/client/proxy.ts +170 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useProductInstances.ts +174 -0
- package/src/index.ts +11 -0
- package/src/mutations/accounts.ts +102 -1
- package/src/mutations/brands.ts +102 -1
- package/src/mutations/index.ts +23 -0
- package/src/mutations/productInstances/index.ts +14 -0
- package/src/mutations/productInstances/productInstances.integration.test.ts +621 -0
- package/src/mutations/productInstances/productInstances.test.ts +680 -0
- package/src/mutations/productInstances/productInstances.ts +280 -0
- package/src/mutations/systemSettings.ts +130 -0
- package/src/mutations/users.ts +102 -1
- package/src/queries/index.ts +9 -0
- package/src/queries/systemSettings.ts +115 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for ProductInstance Mutations
|
|
3
|
+
*
|
|
4
|
+
* Tests the mutation functions for creating, updating, and deleting product instances.
|
|
5
|
+
* Verifies proper authentication, error handling, and data transformation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
createProductInstance,
|
|
11
|
+
updateProductInstance,
|
|
12
|
+
deleteProductInstance,
|
|
13
|
+
toggleProductInstanceEnabled,
|
|
14
|
+
type CreateProductInstanceInput,
|
|
15
|
+
type UpdateProductInstanceInput,
|
|
16
|
+
} from './productInstances';
|
|
17
|
+
import { AppError } from '@htlkg/core/errors';
|
|
18
|
+
import * as auth from '@htlkg/core/auth';
|
|
19
|
+
|
|
20
|
+
// Mock the auth module
|
|
21
|
+
vi.mock('@htlkg/core/auth', () => ({
|
|
22
|
+
getClientUser: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Mock the utils module
|
|
26
|
+
vi.mock('@htlkg/core/utils', () => ({
|
|
27
|
+
getCurrentTimestamp: vi.fn(() => '2024-01-15T10:00:00.000Z'),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
describe('ProductInstance Mutations', () => {
|
|
31
|
+
const mockUserId = 'test-user-123';
|
|
32
|
+
const mockTimestamp = '2024-01-15T10:00:00.000Z';
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
// Mock getClientUser to return a user with the expected email
|
|
37
|
+
vi.mocked(auth.getClientUser).mockResolvedValue({
|
|
38
|
+
username: 'testuser',
|
|
39
|
+
email: mockUserId,
|
|
40
|
+
brandIds: [],
|
|
41
|
+
accountIds: [],
|
|
42
|
+
isAdmin: false,
|
|
43
|
+
isSuperAdmin: false,
|
|
44
|
+
roles: [],
|
|
45
|
+
});
|
|
46
|
+
// Mock Date for consistent timestamps
|
|
47
|
+
vi.useFakeTimers();
|
|
48
|
+
vi.setSystemTime(new Date(mockTimestamp));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.useRealTimers();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('createProductInstance', () => {
|
|
56
|
+
const validInput: CreateProductInstanceInput = {
|
|
57
|
+
productId: 'product-123',
|
|
58
|
+
productName: 'Test Product',
|
|
59
|
+
brandId: 'brand-456',
|
|
60
|
+
accountId: 'account-789',
|
|
61
|
+
enabled: true,
|
|
62
|
+
config: { apiKey: 'test-key', maxRequests: 100 },
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
it('should successfully create a product instance', async () => {
|
|
67
|
+
const mockClient = {
|
|
68
|
+
models: {
|
|
69
|
+
ProductInstance: {
|
|
70
|
+
create: vi.fn().mockResolvedValue({
|
|
71
|
+
data: {
|
|
72
|
+
id: 'instance-001',
|
|
73
|
+
...validInput,
|
|
74
|
+
config: JSON.stringify(validInput.config),
|
|
75
|
+
lastUpdated: mockTimestamp,
|
|
76
|
+
updatedBy: mockUserId,
|
|
77
|
+
},
|
|
78
|
+
errors: null,
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = await createProductInstance(mockClient, validInput);
|
|
85
|
+
|
|
86
|
+
expect(result).toBeTruthy();
|
|
87
|
+
expect(result?.id).toBe('instance-001');
|
|
88
|
+
expect(result?.productId).toBe('product-123');
|
|
89
|
+
expect(result?.updatedBy).toBe(mockUserId);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should call getClientUser to get the user identifier', async () => {
|
|
93
|
+
const mockClient = {
|
|
94
|
+
models: {
|
|
95
|
+
ProductInstance: {
|
|
96
|
+
create: vi.fn().mockResolvedValue({
|
|
97
|
+
data: { id: 'test-id' },
|
|
98
|
+
errors: null,
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await createProductInstance(mockClient, validInput);
|
|
105
|
+
|
|
106
|
+
expect(auth.getClientUser).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should NOT pass authMode to the GraphQL client', async () => {
|
|
110
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
111
|
+
data: { id: 'test-id' },
|
|
112
|
+
errors: null,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const mockClient = {
|
|
116
|
+
models: {
|
|
117
|
+
ProductInstance: {
|
|
118
|
+
create: mockCreate,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await createProductInstance(mockClient, validInput);
|
|
124
|
+
|
|
125
|
+
// Verify create was called with ONLY one argument (no authMode)
|
|
126
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(mockCreate).toHaveBeenCalledWith(
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
productId: 'product-123',
|
|
130
|
+
productName: 'Test Product',
|
|
131
|
+
brandId: 'brand-456',
|
|
132
|
+
accountId: 'account-789',
|
|
133
|
+
enabled: true,
|
|
134
|
+
version: '1.0.0',
|
|
135
|
+
lastUpdated: mockTimestamp,
|
|
136
|
+
updatedBy: mockUserId,
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Verify NO second parameter with authMode
|
|
141
|
+
expect(mockCreate.mock.calls[0]).toHaveLength(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should serialize config as JSON string', async () => {
|
|
145
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
146
|
+
data: { id: 'test-id' },
|
|
147
|
+
errors: null,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const mockClient = {
|
|
151
|
+
models: {
|
|
152
|
+
ProductInstance: {
|
|
153
|
+
create: mockCreate,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const configInput = { apiKey: 'test', nested: { value: 123 } };
|
|
159
|
+
await createProductInstance(mockClient, {
|
|
160
|
+
...validInput,
|
|
161
|
+
config: configInput,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const callArgs = mockCreate.mock.calls[0][0];
|
|
165
|
+
expect(callArgs.config).toBe(JSON.stringify(configInput));
|
|
166
|
+
expect(typeof callArgs.config).toBe('string');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should use current timestamp when lastUpdated is not provided', async () => {
|
|
170
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
171
|
+
data: { id: 'test-id' },
|
|
172
|
+
errors: null,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const mockClient = {
|
|
176
|
+
models: {
|
|
177
|
+
ProductInstance: {
|
|
178
|
+
create: mockCreate,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await createProductInstance(mockClient, validInput);
|
|
184
|
+
|
|
185
|
+
const callArgs = mockCreate.mock.calls[0][0];
|
|
186
|
+
expect(callArgs.lastUpdated).toBe(mockTimestamp);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should use provided updatedBy if specified', async () => {
|
|
190
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
191
|
+
data: { id: 'test-id' },
|
|
192
|
+
errors: null,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const mockClient = {
|
|
196
|
+
models: {
|
|
197
|
+
ProductInstance: {
|
|
198
|
+
create: mockCreate,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const customUserId = 'custom-user-456';
|
|
204
|
+
await createProductInstance(mockClient, {
|
|
205
|
+
...validInput,
|
|
206
|
+
updatedBy: customUserId,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const callArgs = mockCreate.mock.calls[0][0];
|
|
210
|
+
expect(callArgs.updatedBy).toBe(customUserId);
|
|
211
|
+
// getClientUser should not be called since updatedBy is provided
|
|
212
|
+
expect(auth.getClientUser).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should throw AppError when GraphQL returns errors', async () => {
|
|
216
|
+
const mockClient = {
|
|
217
|
+
models: {
|
|
218
|
+
ProductInstance: {
|
|
219
|
+
create: vi.fn().mockResolvedValue({
|
|
220
|
+
data: null,
|
|
221
|
+
errors: [
|
|
222
|
+
{ message: 'Validation error', path: ['input', 'productId'] },
|
|
223
|
+
],
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
await expect(
|
|
230
|
+
createProductInstance(mockClient, validInput)
|
|
231
|
+
).rejects.toThrow(AppError);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await createProductInstance(mockClient, validInput);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
expect(error).toBeInstanceOf(AppError);
|
|
237
|
+
expect((error as AppError).code).toBe('PRODUCT_INSTANCE_CREATE_ERROR');
|
|
238
|
+
expect((error as AppError).statusCode).toBe(500);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should wrap non-AppError exceptions in AppError', async () => {
|
|
243
|
+
const mockClient = {
|
|
244
|
+
models: {
|
|
245
|
+
ProductInstance: {
|
|
246
|
+
create: vi.fn().mockRejectedValue(new Error('Network error')),
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
await expect(
|
|
252
|
+
createProductInstance(mockClient, validInput)
|
|
253
|
+
).rejects.toThrow(AppError);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await createProductInstance(mockClient, validInput);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
expect(error).toBeInstanceOf(AppError);
|
|
259
|
+
expect((error as AppError).code).toBe('PRODUCT_INSTANCE_CREATE_ERROR');
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should handle Vue Proxy objects in config', async () => {
|
|
264
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
265
|
+
data: { id: 'test-id' },
|
|
266
|
+
errors: null,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const mockClient = {
|
|
270
|
+
models: {
|
|
271
|
+
ProductInstance: {
|
|
272
|
+
create: mockCreate,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Simulate Vue Proxy-like object
|
|
278
|
+
const proxyConfig = { apiKey: 'test', __v_isProxy: true };
|
|
279
|
+
await createProductInstance(mockClient, {
|
|
280
|
+
...validInput,
|
|
281
|
+
config: proxyConfig,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const callArgs = mockCreate.mock.calls[0][0];
|
|
285
|
+
// Should be properly serialized despite proxy
|
|
286
|
+
expect(typeof callArgs.config).toBe('string');
|
|
287
|
+
const parsed = JSON.parse(callArgs.config);
|
|
288
|
+
expect(parsed.apiKey).toBe('test');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('updateProductInstance', () => {
|
|
293
|
+
const validInput: UpdateProductInstanceInput = {
|
|
294
|
+
id: 'instance-123',
|
|
295
|
+
enabled: false,
|
|
296
|
+
config: { apiKey: 'updated-key', maxRequests: 200 },
|
|
297
|
+
version: '1.1.0',
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
it('should successfully update a product instance', async () => {
|
|
301
|
+
const mockClient = {
|
|
302
|
+
models: {
|
|
303
|
+
ProductInstance: {
|
|
304
|
+
update: vi.fn().mockResolvedValue({
|
|
305
|
+
data: {
|
|
306
|
+
...validInput,
|
|
307
|
+
config: JSON.stringify(validInput.config),
|
|
308
|
+
lastUpdated: mockTimestamp,
|
|
309
|
+
updatedBy: mockUserId,
|
|
310
|
+
},
|
|
311
|
+
errors: null,
|
|
312
|
+
}),
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const result = await updateProductInstance(mockClient, validInput);
|
|
318
|
+
|
|
319
|
+
expect(result).toBeTruthy();
|
|
320
|
+
expect(result?.id).toBe('instance-123');
|
|
321
|
+
expect(result?.enabled).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should NOT pass authMode to the GraphQL client', async () => {
|
|
325
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
326
|
+
data: { id: 'test-id' },
|
|
327
|
+
errors: null,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const mockClient = {
|
|
331
|
+
models: {
|
|
332
|
+
ProductInstance: {
|
|
333
|
+
update: mockUpdate,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
await updateProductInstance(mockClient, validInput);
|
|
339
|
+
|
|
340
|
+
// Verify NO second parameter with authMode
|
|
341
|
+
expect(mockUpdate.mock.calls[0]).toHaveLength(1);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should add timestamp and user metadata', async () => {
|
|
345
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
346
|
+
data: { id: 'test-id' },
|
|
347
|
+
errors: null,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const mockClient = {
|
|
351
|
+
models: {
|
|
352
|
+
ProductInstance: {
|
|
353
|
+
update: mockUpdate,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
await updateProductInstance(mockClient, validInput);
|
|
359
|
+
|
|
360
|
+
const callArgs = mockUpdate.mock.calls[0][0];
|
|
361
|
+
expect(callArgs.lastUpdated).toBe(mockTimestamp);
|
|
362
|
+
expect(callArgs.updatedBy).toBe(mockUserId);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should serialize config as JSON string', async () => {
|
|
366
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
367
|
+
data: { id: 'test-id' },
|
|
368
|
+
errors: null,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const mockClient = {
|
|
372
|
+
models: {
|
|
373
|
+
ProductInstance: {
|
|
374
|
+
update: mockUpdate,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
await updateProductInstance(mockClient, validInput);
|
|
380
|
+
|
|
381
|
+
const callArgs = mockUpdate.mock.calls[0][0];
|
|
382
|
+
expect(typeof callArgs.config).toBe('string');
|
|
383
|
+
const parsed = JSON.parse(callArgs.config);
|
|
384
|
+
expect(parsed.apiKey).toBe('updated-key');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should throw AppError on GraphQL errors', async () => {
|
|
388
|
+
const mockClient = {
|
|
389
|
+
models: {
|
|
390
|
+
ProductInstance: {
|
|
391
|
+
update: vi.fn().mockResolvedValue({
|
|
392
|
+
data: null,
|
|
393
|
+
errors: [{ message: 'Not found' }],
|
|
394
|
+
}),
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
await expect(
|
|
400
|
+
updateProductInstance(mockClient, validInput)
|
|
401
|
+
).rejects.toThrow(AppError);
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
await updateProductInstance(mockClient, validInput);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
expect((error as AppError).code).toBe('PRODUCT_INSTANCE_UPDATE_ERROR');
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should preserve existing AppError instances', async () => {
|
|
411
|
+
const originalError = new AppError('Original error', 'CUSTOM_CODE', 400);
|
|
412
|
+
const mockClient = {
|
|
413
|
+
models: {
|
|
414
|
+
ProductInstance: {
|
|
415
|
+
update: vi.fn().mockRejectedValue(originalError),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
await expect(
|
|
421
|
+
updateProductInstance(mockClient, validInput)
|
|
422
|
+
).rejects.toThrow(originalError);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe('deleteProductInstance', () => {
|
|
427
|
+
const testId = 'instance-to-delete-123';
|
|
428
|
+
|
|
429
|
+
it('should successfully delete a product instance', async () => {
|
|
430
|
+
const mockClient = {
|
|
431
|
+
models: {
|
|
432
|
+
ProductInstance: {
|
|
433
|
+
delete: vi.fn().mockResolvedValue({
|
|
434
|
+
data: { id: testId },
|
|
435
|
+
errors: null,
|
|
436
|
+
}),
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const result = await deleteProductInstance(mockClient, testId);
|
|
442
|
+
|
|
443
|
+
expect(result).toBe(true);
|
|
444
|
+
expect(mockClient.models.ProductInstance.delete).toHaveBeenCalledWith({
|
|
445
|
+
id: testId,
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should NOT pass authMode to the GraphQL client', async () => {
|
|
450
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
451
|
+
data: { id: testId },
|
|
452
|
+
errors: null,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const mockClient = {
|
|
456
|
+
models: {
|
|
457
|
+
ProductInstance: {
|
|
458
|
+
delete: mockDelete,
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
await deleteProductInstance(mockClient, testId);
|
|
464
|
+
|
|
465
|
+
// Verify delete was called with ONLY one argument
|
|
466
|
+
expect(mockDelete.mock.calls[0]).toHaveLength(1);
|
|
467
|
+
expect(mockDelete).toHaveBeenCalledWith({ id: testId });
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should throw AppError on GraphQL errors', async () => {
|
|
471
|
+
const mockClient = {
|
|
472
|
+
models: {
|
|
473
|
+
ProductInstance: {
|
|
474
|
+
delete: vi.fn().mockResolvedValue({
|
|
475
|
+
data: null,
|
|
476
|
+
errors: [{ message: 'Permission denied' }],
|
|
477
|
+
}),
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
await expect(
|
|
483
|
+
deleteProductInstance(mockClient, testId)
|
|
484
|
+
).rejects.toThrow(AppError);
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
await deleteProductInstance(mockClient, testId);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
expect((error as AppError).code).toBe('PRODUCT_INSTANCE_DELETE_ERROR');
|
|
490
|
+
expect((error as AppError).statusCode).toBe(500);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should wrap non-AppError exceptions in AppError', async () => {
|
|
495
|
+
const mockClient = {
|
|
496
|
+
models: {
|
|
497
|
+
ProductInstance: {
|
|
498
|
+
delete: vi.fn().mockRejectedValue(new Error('Database error')),
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
await expect(
|
|
504
|
+
deleteProductInstance(mockClient, testId)
|
|
505
|
+
).rejects.toThrow(AppError);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe('toggleProductInstanceEnabled', () => {
|
|
510
|
+
const testId = 'instance-toggle-123';
|
|
511
|
+
|
|
512
|
+
it('should successfully enable a product instance', async () => {
|
|
513
|
+
const mockClient = {
|
|
514
|
+
models: {
|
|
515
|
+
ProductInstance: {
|
|
516
|
+
update: vi.fn().mockResolvedValue({
|
|
517
|
+
data: {
|
|
518
|
+
id: testId,
|
|
519
|
+
enabled: true,
|
|
520
|
+
lastUpdated: mockTimestamp,
|
|
521
|
+
updatedBy: mockUserId,
|
|
522
|
+
},
|
|
523
|
+
errors: null,
|
|
524
|
+
}),
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const result = await toggleProductInstanceEnabled(mockClient, testId, true);
|
|
530
|
+
|
|
531
|
+
expect(result).toBeTruthy();
|
|
532
|
+
expect(result?.enabled).toBe(true);
|
|
533
|
+
expect(result?.updatedBy).toBe(mockUserId);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should successfully disable a product instance', async () => {
|
|
537
|
+
const mockClient = {
|
|
538
|
+
models: {
|
|
539
|
+
ProductInstance: {
|
|
540
|
+
update: vi.fn().mockResolvedValue({
|
|
541
|
+
data: {
|
|
542
|
+
id: testId,
|
|
543
|
+
enabled: false,
|
|
544
|
+
lastUpdated: mockTimestamp,
|
|
545
|
+
updatedBy: mockUserId,
|
|
546
|
+
},
|
|
547
|
+
errors: null,
|
|
548
|
+
}),
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const result = await toggleProductInstanceEnabled(mockClient, testId, false);
|
|
554
|
+
|
|
555
|
+
expect(result).toBeTruthy();
|
|
556
|
+
expect(result?.enabled).toBe(false);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should NOT pass authMode to the GraphQL client', async () => {
|
|
560
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
561
|
+
data: { id: testId, enabled: true },
|
|
562
|
+
errors: null,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const mockClient = {
|
|
566
|
+
models: {
|
|
567
|
+
ProductInstance: {
|
|
568
|
+
update: mockUpdate,
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
await toggleProductInstanceEnabled(mockClient, testId, true);
|
|
574
|
+
|
|
575
|
+
// Verify NO second parameter with authMode
|
|
576
|
+
expect(mockUpdate.mock.calls[0]).toHaveLength(1);
|
|
577
|
+
expect(mockUpdate).toHaveBeenCalledWith({
|
|
578
|
+
id: testId,
|
|
579
|
+
enabled: true,
|
|
580
|
+
lastUpdated: mockTimestamp,
|
|
581
|
+
updatedBy: mockUserId,
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should call getClientUser for user tracking', async () => {
|
|
586
|
+
const mockClient = {
|
|
587
|
+
models: {
|
|
588
|
+
ProductInstance: {
|
|
589
|
+
update: vi.fn().mockResolvedValue({
|
|
590
|
+
data: { id: testId, enabled: true },
|
|
591
|
+
errors: null,
|
|
592
|
+
}),
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
await toggleProductInstanceEnabled(mockClient, testId, true);
|
|
598
|
+
|
|
599
|
+
expect(auth.getClientUser).toHaveBeenCalledTimes(1);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should throw AppError on GraphQL errors', async () => {
|
|
603
|
+
const mockClient = {
|
|
604
|
+
models: {
|
|
605
|
+
ProductInstance: {
|
|
606
|
+
update: vi.fn().mockResolvedValue({
|
|
607
|
+
data: null,
|
|
608
|
+
errors: [{ message: 'Instance not found' }],
|
|
609
|
+
}),
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
await expect(
|
|
615
|
+
toggleProductInstanceEnabled(mockClient, testId, true)
|
|
616
|
+
).rejects.toThrow(AppError);
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
await toggleProductInstanceEnabled(mockClient, testId, true);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
expect((error as AppError).code).toBe('PRODUCT_INSTANCE_TOGGLE_ERROR');
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should wrap non-AppError exceptions in AppError', async () => {
|
|
626
|
+
const mockClient = {
|
|
627
|
+
models: {
|
|
628
|
+
ProductInstance: {
|
|
629
|
+
update: vi.fn().mockRejectedValue(new Error('Connection error')),
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
await expect(
|
|
635
|
+
toggleProductInstanceEnabled(mockClient, testId, true)
|
|
636
|
+
).rejects.toThrow(AppError);
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
describe('Authentication and Authorization', () => {
|
|
641
|
+
it('all mutations should use Cognito user pool auth (no apiKey)', async () => {
|
|
642
|
+
const mockClient = {
|
|
643
|
+
models: {
|
|
644
|
+
ProductInstance: {
|
|
645
|
+
create: vi.fn().mockResolvedValue({ data: { id: '1' }, errors: null }),
|
|
646
|
+
update: vi.fn().mockResolvedValue({ data: { id: '1' }, errors: null }),
|
|
647
|
+
delete: vi.fn().mockResolvedValue({ data: { id: '1' }, errors: null }),
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// Test create
|
|
653
|
+
await createProductInstance(mockClient, {
|
|
654
|
+
productId: 'p1',
|
|
655
|
+
productName: 'Product',
|
|
656
|
+
brandId: 'b1',
|
|
657
|
+
accountId: 'a1',
|
|
658
|
+
enabled: true,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Test update
|
|
662
|
+
await updateProductInstance(mockClient, {
|
|
663
|
+
id: 'i1',
|
|
664
|
+
enabled: true,
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Test delete
|
|
668
|
+
await deleteProductInstance(mockClient, 'i1');
|
|
669
|
+
|
|
670
|
+
// Test toggle
|
|
671
|
+
await toggleProductInstanceEnabled(mockClient, 'i1', true);
|
|
672
|
+
|
|
673
|
+
// Verify none of them passed authMode as second parameter
|
|
674
|
+
expect(mockClient.models.ProductInstance.create.mock.calls[0]).toHaveLength(1);
|
|
675
|
+
expect(mockClient.models.ProductInstance.update.mock.calls[0]).toHaveLength(1);
|
|
676
|
+
expect(mockClient.models.ProductInstance.update.mock.calls[1]).toHaveLength(1);
|
|
677
|
+
expect(mockClient.models.ProductInstance.delete.mock.calls[0]).toHaveLength(1);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
});
|