@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,621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests for ProductInstance Mutations
|
|
3
|
+
*
|
|
4
|
+
* Tests the mutation functions with a more realistic GraphQL client setup
|
|
5
|
+
* to verify end-to-end behavior including serialization and error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
createProductInstance,
|
|
11
|
+
updateProductInstance,
|
|
12
|
+
deleteProductInstance,
|
|
13
|
+
toggleProductInstanceEnabled,
|
|
14
|
+
} from './productInstances';
|
|
15
|
+
import { AppError } from '@htlkg/core/errors';
|
|
16
|
+
import * as auth from '@htlkg/core/auth';
|
|
17
|
+
|
|
18
|
+
// Mock the auth module
|
|
19
|
+
vi.mock('@htlkg/core/auth', () => ({
|
|
20
|
+
getClientUser: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock the utils module
|
|
24
|
+
vi.mock('@htlkg/core/utils', () => ({
|
|
25
|
+
getCurrentTimestamp: vi.fn(() => '2024-01-15T12:00:00.000Z'),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a more realistic mock GraphQL client that simulates
|
|
30
|
+
* AWS Amplify's behavior
|
|
31
|
+
*/
|
|
32
|
+
function createMockGraphQLClient(options: {
|
|
33
|
+
createResponse?: any;
|
|
34
|
+
updateResponse?: any;
|
|
35
|
+
deleteResponse?: any;
|
|
36
|
+
shouldDelay?: boolean;
|
|
37
|
+
shouldFail?: boolean;
|
|
38
|
+
}) {
|
|
39
|
+
const { createResponse, updateResponse, deleteResponse, shouldDelay = false, shouldFail = false } = options;
|
|
40
|
+
|
|
41
|
+
const mockCreate = vi.fn(async (input: any) => {
|
|
42
|
+
if (shouldDelay) await new Promise(resolve => setTimeout(resolve, 10));
|
|
43
|
+
if (shouldFail) throw new Error('Network timeout');
|
|
44
|
+
|
|
45
|
+
return createResponse || {
|
|
46
|
+
data: {
|
|
47
|
+
id: `generated-${Date.now()}`,
|
|
48
|
+
...input,
|
|
49
|
+
},
|
|
50
|
+
errors: null,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const mockUpdate = vi.fn(async (input: any) => {
|
|
55
|
+
if (shouldDelay) await new Promise(resolve => setTimeout(resolve, 10));
|
|
56
|
+
if (shouldFail) throw new Error('Network timeout');
|
|
57
|
+
|
|
58
|
+
return updateResponse || {
|
|
59
|
+
data: input,
|
|
60
|
+
errors: null,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const mockDelete = vi.fn(async (input: any) => {
|
|
65
|
+
if (shouldDelay) await new Promise(resolve => setTimeout(resolve, 10));
|
|
66
|
+
if (shouldFail) throw new Error('Network timeout');
|
|
67
|
+
|
|
68
|
+
return deleteResponse || {
|
|
69
|
+
data: { id: input.id },
|
|
70
|
+
errors: null,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
models: {
|
|
76
|
+
ProductInstance: {
|
|
77
|
+
create: mockCreate,
|
|
78
|
+
update: mockUpdate,
|
|
79
|
+
delete: mockDelete,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
_mocks: {
|
|
83
|
+
create: mockCreate,
|
|
84
|
+
update: mockUpdate,
|
|
85
|
+
delete: mockDelete,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('ProductInstance Mutations - Integration Tests', () => {
|
|
91
|
+
const mockUserId = 'integration-user-789';
|
|
92
|
+
const mockTimestamp = '2024-01-15T12:00:00.000Z';
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
vi.clearAllMocks();
|
|
96
|
+
// Mock getClientUser to return a user with the expected email
|
|
97
|
+
vi.mocked(auth.getClientUser).mockResolvedValue({
|
|
98
|
+
username: 'testuser',
|
|
99
|
+
email: mockUserId,
|
|
100
|
+
brandIds: [],
|
|
101
|
+
accountIds: [],
|
|
102
|
+
isAdmin: false,
|
|
103
|
+
isSuperAdmin: false,
|
|
104
|
+
roles: [],
|
|
105
|
+
});
|
|
106
|
+
vi.useFakeTimers();
|
|
107
|
+
vi.setSystemTime(new Date(mockTimestamp));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
vi.useRealTimers();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('End-to-End Flow: Create -> Update -> Toggle -> Delete', () => {
|
|
115
|
+
it('should perform complete lifecycle of a product instance', async () => {
|
|
116
|
+
let instanceId: string;
|
|
117
|
+
const client = createMockGraphQLClient({});
|
|
118
|
+
|
|
119
|
+
// Step 1: Create
|
|
120
|
+
const created = await createProductInstance(client, {
|
|
121
|
+
productId: 'integration-product-1',
|
|
122
|
+
productName: 'Integration Test Product',
|
|
123
|
+
brandId: 'integration-brand-1',
|
|
124
|
+
accountId: 'integration-account-1',
|
|
125
|
+
enabled: true,
|
|
126
|
+
config: { testMode: true, apiKey: 'test-key' },
|
|
127
|
+
version: '1.0.0',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(created).toBeTruthy();
|
|
131
|
+
expect(created?.productId).toBe('integration-product-1');
|
|
132
|
+
expect(created?.updatedBy).toBe(mockUserId);
|
|
133
|
+
instanceId = created!.id;
|
|
134
|
+
|
|
135
|
+
// Step 2: Update configuration
|
|
136
|
+
const updated = await updateProductInstance(client, {
|
|
137
|
+
id: instanceId,
|
|
138
|
+
config: { testMode: false, apiKey: 'updated-key', maxRequests: 1000 },
|
|
139
|
+
version: '1.1.0',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(updated).toBeTruthy();
|
|
143
|
+
expect(updated?.id).toBe(instanceId);
|
|
144
|
+
expect(updated?.version).toBe('1.1.0');
|
|
145
|
+
|
|
146
|
+
// Step 3: Toggle enabled status
|
|
147
|
+
const toggled = await toggleProductInstanceEnabled(client, instanceId, false);
|
|
148
|
+
|
|
149
|
+
expect(toggled).toBeTruthy();
|
|
150
|
+
expect(toggled?.id).toBe(instanceId);
|
|
151
|
+
expect(toggled?.enabled).toBe(false);
|
|
152
|
+
|
|
153
|
+
// Step 4: Delete
|
|
154
|
+
const deleted = await deleteProductInstance(client, instanceId);
|
|
155
|
+
|
|
156
|
+
expect(deleted).toBe(true);
|
|
157
|
+
|
|
158
|
+
// Verify all operations used the correct client methods
|
|
159
|
+
expect(client._mocks.create).toHaveBeenCalledTimes(1);
|
|
160
|
+
expect(client._mocks.update).toHaveBeenCalledTimes(2); // update + toggle
|
|
161
|
+
expect(client._mocks.delete).toHaveBeenCalledTimes(1);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('Concurrent Operations', () => {
|
|
166
|
+
it('should handle multiple mutations in parallel', async () => {
|
|
167
|
+
const client = createMockGraphQLClient({});
|
|
168
|
+
|
|
169
|
+
const operations = [
|
|
170
|
+
createProductInstance(client, {
|
|
171
|
+
productId: 'concurrent-1',
|
|
172
|
+
productName: 'Product 1',
|
|
173
|
+
brandId: 'brand-1',
|
|
174
|
+
accountId: 'account-1',
|
|
175
|
+
enabled: true,
|
|
176
|
+
}),
|
|
177
|
+
createProductInstance(client, {
|
|
178
|
+
productId: 'concurrent-2',
|
|
179
|
+
productName: 'Product 2',
|
|
180
|
+
brandId: 'brand-1',
|
|
181
|
+
accountId: 'account-1',
|
|
182
|
+
enabled: true,
|
|
183
|
+
}),
|
|
184
|
+
createProductInstance(client, {
|
|
185
|
+
productId: 'concurrent-3',
|
|
186
|
+
productName: 'Product 3',
|
|
187
|
+
brandId: 'brand-1',
|
|
188
|
+
accountId: 'account-1',
|
|
189
|
+
enabled: true,
|
|
190
|
+
}),
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const results = await Promise.all(operations);
|
|
194
|
+
|
|
195
|
+
expect(results).toHaveLength(3);
|
|
196
|
+
results.forEach(result => {
|
|
197
|
+
expect(result).toBeTruthy();
|
|
198
|
+
expect(result?.updatedBy).toBe(mockUserId);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle mixed success and failure scenarios', async () => {
|
|
203
|
+
const successClient = createMockGraphQLClient({});
|
|
204
|
+
const failClient = createMockGraphQLClient({
|
|
205
|
+
createResponse: {
|
|
206
|
+
data: null,
|
|
207
|
+
errors: [{ message: 'Validation failed' }],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const results = await Promise.allSettled([
|
|
212
|
+
createProductInstance(successClient, {
|
|
213
|
+
productId: 'success-1',
|
|
214
|
+
productName: 'Success Product',
|
|
215
|
+
brandId: 'brand-1',
|
|
216
|
+
accountId: 'account-1',
|
|
217
|
+
enabled: true,
|
|
218
|
+
}),
|
|
219
|
+
createProductInstance(failClient, {
|
|
220
|
+
productId: 'fail-1',
|
|
221
|
+
productName: 'Fail Product',
|
|
222
|
+
brandId: 'brand-1',
|
|
223
|
+
accountId: 'account-1',
|
|
224
|
+
enabled: true,
|
|
225
|
+
}),
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
expect(results[0].status).toBe('fulfilled');
|
|
229
|
+
expect(results[1].status).toBe('rejected');
|
|
230
|
+
expect((results[1] as PromiseRejectedResult).reason).toBeInstanceOf(AppError);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('Complex Config Serialization', () => {
|
|
235
|
+
it('should handle nested objects in config', async () => {
|
|
236
|
+
const client = createMockGraphQLClient({});
|
|
237
|
+
|
|
238
|
+
const complexConfig = {
|
|
239
|
+
authentication: {
|
|
240
|
+
type: 'oauth',
|
|
241
|
+
credentials: {
|
|
242
|
+
clientId: 'test-client',
|
|
243
|
+
clientSecret: 'test-secret',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
endpoints: {
|
|
247
|
+
production: 'https://api.example.com',
|
|
248
|
+
staging: 'https://staging.example.com',
|
|
249
|
+
},
|
|
250
|
+
features: ['feature1', 'feature2', 'feature3'],
|
|
251
|
+
metadata: {
|
|
252
|
+
createdAt: '2024-01-01',
|
|
253
|
+
tags: ['tag1', 'tag2'],
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const result = await createProductInstance(client, {
|
|
258
|
+
productId: 'complex-config-test',
|
|
259
|
+
productName: 'Complex Config Product',
|
|
260
|
+
brandId: 'brand-1',
|
|
261
|
+
accountId: 'account-1',
|
|
262
|
+
enabled: true,
|
|
263
|
+
config: complexConfig,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Verify the config was serialized
|
|
267
|
+
const createCall = client._mocks.create.mock.calls[0][0];
|
|
268
|
+
expect(typeof createCall.config).toBe('string');
|
|
269
|
+
|
|
270
|
+
// Verify it can be deserialized correctly
|
|
271
|
+
const parsedConfig = JSON.parse(createCall.config);
|
|
272
|
+
expect(parsedConfig).toEqual(complexConfig);
|
|
273
|
+
expect(parsedConfig.authentication.credentials.clientId).toBe('test-client');
|
|
274
|
+
expect(parsedConfig.features).toHaveLength(3);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should handle arrays in config', async () => {
|
|
278
|
+
const client = createMockGraphQLClient({});
|
|
279
|
+
|
|
280
|
+
const arrayConfig = {
|
|
281
|
+
allowedDomains: ['example.com', 'test.com', 'demo.com'],
|
|
282
|
+
ipWhitelist: ['192.168.1.1', '10.0.0.1'],
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
await createProductInstance(client, {
|
|
286
|
+
productId: 'array-config-test',
|
|
287
|
+
productName: 'Array Config Product',
|
|
288
|
+
brandId: 'brand-1',
|
|
289
|
+
accountId: 'account-1',
|
|
290
|
+
enabled: true,
|
|
291
|
+
config: arrayConfig,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const createCall = client._mocks.create.mock.calls[0][0];
|
|
295
|
+
const parsedConfig = JSON.parse(createCall.config);
|
|
296
|
+
|
|
297
|
+
expect(Array.isArray(parsedConfig.allowedDomains)).toBe(true);
|
|
298
|
+
expect(parsedConfig.allowedDomains).toHaveLength(3);
|
|
299
|
+
expect(parsedConfig.ipWhitelist[0]).toBe('192.168.1.1');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should handle special characters in config values', async () => {
|
|
303
|
+
const client = createMockGraphQLClient({});
|
|
304
|
+
|
|
305
|
+
const specialCharsConfig = {
|
|
306
|
+
apiKey: 'key-with-special-chars-!@#$%^&*()',
|
|
307
|
+
description: 'Text with "quotes" and \'apostrophes\'',
|
|
308
|
+
url: 'https://example.com?param=value&other=123',
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
await updateProductInstance(client, {
|
|
312
|
+
id: 'special-chars-test',
|
|
313
|
+
config: specialCharsConfig,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const updateCall = client._mocks.update.mock.calls[0][0];
|
|
317
|
+
const parsedConfig = JSON.parse(updateCall.config);
|
|
318
|
+
|
|
319
|
+
expect(parsedConfig.apiKey).toBe('key-with-special-chars-!@#$%^&*()');
|
|
320
|
+
expect(parsedConfig.description).toContain('quotes');
|
|
321
|
+
expect(parsedConfig.url).toContain('?param=value');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('Error Scenarios', () => {
|
|
326
|
+
it('should handle GraphQL errors with detailed context', async () => {
|
|
327
|
+
const client = createMockGraphQLClient({
|
|
328
|
+
createResponse: {
|
|
329
|
+
data: null,
|
|
330
|
+
errors: [
|
|
331
|
+
{
|
|
332
|
+
message: 'Product not found',
|
|
333
|
+
path: ['createProductInstance', 'productId'],
|
|
334
|
+
extensions: {
|
|
335
|
+
code: 'NOT_FOUND',
|
|
336
|
+
details: { productId: 'invalid-product-123' },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
await createProductInstance(client, {
|
|
345
|
+
productId: 'invalid-product-123',
|
|
346
|
+
productName: 'Invalid Product',
|
|
347
|
+
brandId: 'brand-1',
|
|
348
|
+
accountId: 'account-1',
|
|
349
|
+
enabled: true,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect.fail('Should have thrown an error');
|
|
353
|
+
} catch (error) {
|
|
354
|
+
expect(error).toBeInstanceOf(AppError);
|
|
355
|
+
expect((error as AppError).code).toBe('PRODUCT_INSTANCE_CREATE_ERROR');
|
|
356
|
+
expect((error as AppError).message).toContain('Failed to create product instance');
|
|
357
|
+
expect((error as AppError).details).toBeDefined();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should handle network timeouts', async () => {
|
|
362
|
+
const client = createMockGraphQLClient({
|
|
363
|
+
shouldFail: true,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await updateProductInstance(client, {
|
|
368
|
+
id: 'timeout-test',
|
|
369
|
+
enabled: true,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect.fail('Should have thrown an error');
|
|
373
|
+
} catch (error) {
|
|
374
|
+
expect(error).toBeInstanceOf(AppError);
|
|
375
|
+
expect((error as AppError).code).toBe('PRODUCT_INSTANCE_UPDATE_ERROR');
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should handle missing required fields', async () => {
|
|
380
|
+
const client = createMockGraphQLClient({
|
|
381
|
+
createResponse: {
|
|
382
|
+
data: null,
|
|
383
|
+
errors: [
|
|
384
|
+
{
|
|
385
|
+
message: 'Variable "$input" of required type "CreateProductInstanceInput!" was not provided.',
|
|
386
|
+
extensions: { code: 'BAD_USER_INPUT' },
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await createProductInstance(client, {
|
|
394
|
+
productId: '',
|
|
395
|
+
productName: '',
|
|
396
|
+
brandId: '',
|
|
397
|
+
accountId: '',
|
|
398
|
+
enabled: true,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect.fail('Should have thrown an error');
|
|
402
|
+
} catch (error) {
|
|
403
|
+
expect(error).toBeInstanceOf(AppError);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('User Context and Tracking', () => {
|
|
409
|
+
it('should track different users across operations', async () => {
|
|
410
|
+
const client = createMockGraphQLClient({});
|
|
411
|
+
|
|
412
|
+
// User 1 creates
|
|
413
|
+
vi.mocked(auth.getClientUser).mockResolvedValueOnce({
|
|
414
|
+
username: 'user1',
|
|
415
|
+
email: 'user-001',
|
|
416
|
+
brandIds: [],
|
|
417
|
+
accountIds: [],
|
|
418
|
+
isAdmin: false,
|
|
419
|
+
isSuperAdmin: false,
|
|
420
|
+
roles: [],
|
|
421
|
+
});
|
|
422
|
+
await createProductInstance(client, {
|
|
423
|
+
productId: 'tracking-test-1',
|
|
424
|
+
productName: 'Tracking Product',
|
|
425
|
+
brandId: 'brand-1',
|
|
426
|
+
accountId: 'account-1',
|
|
427
|
+
enabled: true,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(client._mocks.create.mock.calls[0][0].updatedBy).toBe('user-001');
|
|
431
|
+
|
|
432
|
+
// User 2 updates
|
|
433
|
+
vi.mocked(auth.getClientUser).mockResolvedValueOnce({
|
|
434
|
+
username: 'user2',
|
|
435
|
+
email: 'user-002',
|
|
436
|
+
brandIds: [],
|
|
437
|
+
accountIds: [],
|
|
438
|
+
isAdmin: false,
|
|
439
|
+
isSuperAdmin: false,
|
|
440
|
+
roles: [],
|
|
441
|
+
});
|
|
442
|
+
await updateProductInstance(client, {
|
|
443
|
+
id: 'tracking-test-1',
|
|
444
|
+
enabled: false,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
expect(client._mocks.update.mock.calls[0][0].updatedBy).toBe('user-002');
|
|
448
|
+
|
|
449
|
+
// User 3 toggles
|
|
450
|
+
vi.mocked(auth.getClientUser).mockResolvedValueOnce({
|
|
451
|
+
username: 'user3',
|
|
452
|
+
email: 'user-003',
|
|
453
|
+
brandIds: [],
|
|
454
|
+
accountIds: [],
|
|
455
|
+
isAdmin: false,
|
|
456
|
+
isSuperAdmin: false,
|
|
457
|
+
roles: [],
|
|
458
|
+
});
|
|
459
|
+
await toggleProductInstanceEnabled(client, 'tracking-test-1', true);
|
|
460
|
+
|
|
461
|
+
expect(client._mocks.update.mock.calls[1][0].updatedBy).toBe('user-003');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should handle scenarios where getClientUser returns null (unauthenticated)', async () => {
|
|
465
|
+
const client = createMockGraphQLClient({});
|
|
466
|
+
|
|
467
|
+
// Mock getClientUser to return null (user not authenticated)
|
|
468
|
+
vi.mocked(auth.getClientUser).mockResolvedValueOnce(null);
|
|
469
|
+
|
|
470
|
+
// Should still work, using "system" as fallback
|
|
471
|
+
const result = await createProductInstance(client, {
|
|
472
|
+
productId: 'auth-fail-test',
|
|
473
|
+
productName: 'Auth Fail Product',
|
|
474
|
+
brandId: 'brand-1',
|
|
475
|
+
accountId: 'account-1',
|
|
476
|
+
enabled: true,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
expect(result).toBeTruthy();
|
|
480
|
+
expect(client._mocks.create.mock.calls[0][0].updatedBy).toBe('system');
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('Performance and Edge Cases', () => {
|
|
485
|
+
it('should handle large config objects', async () => {
|
|
486
|
+
const client = createMockGraphQLClient({});
|
|
487
|
+
|
|
488
|
+
// Create a large config with 100 properties
|
|
489
|
+
const largeConfig: Record<string, any> = {};
|
|
490
|
+
for (let i = 0; i < 100; i++) {
|
|
491
|
+
largeConfig[`property${i}`] = {
|
|
492
|
+
value: `value-${i}`,
|
|
493
|
+
nested: {
|
|
494
|
+
data: Array(10).fill(i),
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const result = await createProductInstance(client, {
|
|
500
|
+
productId: 'large-config-test',
|
|
501
|
+
productName: 'Large Config Product',
|
|
502
|
+
brandId: 'brand-1',
|
|
503
|
+
accountId: 'account-1',
|
|
504
|
+
enabled: true,
|
|
505
|
+
config: largeConfig,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
expect(result).toBeTruthy();
|
|
509
|
+
|
|
510
|
+
const createCall = client._mocks.create.mock.calls[0][0];
|
|
511
|
+
const parsedConfig = JSON.parse(createCall.config);
|
|
512
|
+
expect(Object.keys(parsedConfig)).toHaveLength(100);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should handle rapid sequential updates', async () => {
|
|
516
|
+
// Use real timers for this async test
|
|
517
|
+
vi.useRealTimers();
|
|
518
|
+
|
|
519
|
+
const client = createMockGraphQLClient({ shouldDelay: true });
|
|
520
|
+
|
|
521
|
+
const instanceId = 'rapid-update-test';
|
|
522
|
+
const updates = [];
|
|
523
|
+
|
|
524
|
+
for (let i = 0; i < 5; i++) {
|
|
525
|
+
updates.push(
|
|
526
|
+
updateProductInstance(client, {
|
|
527
|
+
id: instanceId,
|
|
528
|
+
version: `1.${i}.0`,
|
|
529
|
+
})
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const results = await Promise.all(updates);
|
|
534
|
+
|
|
535
|
+
expect(results).toHaveLength(5);
|
|
536
|
+
expect(client._mocks.update).toHaveBeenCalledTimes(5);
|
|
537
|
+
|
|
538
|
+
// Restore fake timers
|
|
539
|
+
vi.useFakeTimers();
|
|
540
|
+
vi.setSystemTime(new Date(mockTimestamp));
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should handle empty config objects', async () => {
|
|
544
|
+
const client = createMockGraphQLClient({});
|
|
545
|
+
|
|
546
|
+
const result = await createProductInstance(client, {
|
|
547
|
+
productId: 'empty-config-test',
|
|
548
|
+
productName: 'Empty Config Product',
|
|
549
|
+
brandId: 'brand-1',
|
|
550
|
+
accountId: 'account-1',
|
|
551
|
+
enabled: true,
|
|
552
|
+
config: {},
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
expect(result).toBeTruthy();
|
|
556
|
+
|
|
557
|
+
const createCall = client._mocks.create.mock.calls[0][0];
|
|
558
|
+
expect(createCall.config).toBe('{}');
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('should handle undefined config', async () => {
|
|
562
|
+
const client = createMockGraphQLClient({});
|
|
563
|
+
|
|
564
|
+
const result = await createProductInstance(client, {
|
|
565
|
+
productId: 'no-config-test',
|
|
566
|
+
productName: 'No Config Product',
|
|
567
|
+
brandId: 'brand-1',
|
|
568
|
+
accountId: 'account-1',
|
|
569
|
+
enabled: true,
|
|
570
|
+
// No config property
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
expect(result).toBeTruthy();
|
|
574
|
+
|
|
575
|
+
const createCall = client._mocks.create.mock.calls[0][0];
|
|
576
|
+
expect(createCall.config).toBeUndefined();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
describe('Idempotency and Retry Logic', () => {
|
|
581
|
+
it('should handle duplicate create requests gracefully', async () => {
|
|
582
|
+
const client = createMockGraphQLClient({
|
|
583
|
+
createResponse: {
|
|
584
|
+
data: null,
|
|
585
|
+
errors: [
|
|
586
|
+
{
|
|
587
|
+
message: 'Duplicate entry',
|
|
588
|
+
extensions: { code: 'DUPLICATE_ENTRY' },
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const input = {
|
|
595
|
+
productId: 'duplicate-test',
|
|
596
|
+
productName: 'Duplicate Product',
|
|
597
|
+
brandId: 'brand-1',
|
|
598
|
+
accountId: 'account-1',
|
|
599
|
+
enabled: true,
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
await expect(createProductInstance(client, input)).rejects.toThrow(AppError);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should support optimistic updates', async () => {
|
|
606
|
+
const client = createMockGraphQLClient({});
|
|
607
|
+
|
|
608
|
+
const instanceId = 'optimistic-test';
|
|
609
|
+
|
|
610
|
+
// Simulate optimistic update - fire and forget pattern
|
|
611
|
+
const updatePromise = toggleProductInstanceEnabled(client, instanceId, true);
|
|
612
|
+
|
|
613
|
+
// Continue with other operations without waiting
|
|
614
|
+
expect(updatePromise).toBeInstanceOf(Promise);
|
|
615
|
+
|
|
616
|
+
// Eventually resolve
|
|
617
|
+
const result = await updatePromise;
|
|
618
|
+
expect(result?.enabled).toBe(true);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
});
|