@friggframework/core 2.0.0--canary.461.61382d8.0 → 2.0.0--canary.461.3d6d8ad.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.
Files changed (120) hide show
  1. package/database/use-cases/check-migration-status-use-case.js +81 -0
  2. package/generated/prisma-mongodb/client.d.ts +1 -0
  3. package/generated/prisma-mongodb/client.js +4 -0
  4. package/generated/prisma-mongodb/default.d.ts +1 -0
  5. package/generated/prisma-mongodb/default.js +4 -0
  6. package/generated/prisma-mongodb/edge.d.ts +1 -0
  7. package/generated/prisma-mongodb/edge.js +336 -0
  8. package/generated/prisma-mongodb/index-browser.js +318 -0
  9. package/generated/prisma-mongodb/index.d.ts +22993 -0
  10. package/generated/prisma-mongodb/index.js +361 -0
  11. package/generated/prisma-mongodb/package.json +183 -0
  12. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  13. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  14. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  15. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  16. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  17. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  18. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  19. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  20. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  21. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  22. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  23. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  24. package/generated/prisma-mongodb/schema.prisma +364 -0
  25. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  26. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  27. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  28. package/generated/prisma-mongodb/wasm.js +343 -0
  29. package/generated/prisma-postgresql/client.d.ts +1 -0
  30. package/generated/prisma-postgresql/client.js +4 -0
  31. package/generated/prisma-postgresql/default.d.ts +1 -0
  32. package/generated/prisma-postgresql/default.js +4 -0
  33. package/generated/prisma-postgresql/edge.d.ts +1 -0
  34. package/generated/prisma-postgresql/edge.js +358 -0
  35. package/generated/prisma-postgresql/index-browser.js +340 -0
  36. package/generated/prisma-postgresql/index.d.ts +25171 -0
  37. package/generated/prisma-postgresql/index.js +383 -0
  38. package/generated/prisma-postgresql/package.json +183 -0
  39. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  40. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  41. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  42. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  43. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  44. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  45. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  46. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  47. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  48. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  49. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  50. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  51. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  52. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  53. package/generated/prisma-postgresql/schema.prisma +347 -0
  54. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  55. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  56. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  57. package/generated/prisma-postgresql/wasm.js +365 -0
  58. package/handlers/routers/db-migration.js +52 -0
  59. package/package.json +5 -5
  60. package/application/commands/integration-commands.test.js +0 -123
  61. package/core/Worker.test.js +0 -159
  62. package/database/encryption/encryption-integration.test.js +0 -553
  63. package/database/encryption/encryption-schema-registry.test.js +0 -392
  64. package/database/encryption/field-encryption-service.test.js +0 -525
  65. package/database/encryption/mongo-decryption-fix-verification.test.js +0 -348
  66. package/database/encryption/postgres-decryption-fix-verification.test.js +0 -371
  67. package/database/encryption/postgres-relation-decryption.test.js +0 -245
  68. package/database/encryption/prisma-encryption-extension.test.js +0 -439
  69. package/database/repositories/migration-status-repository-s3.test.js +0 -158
  70. package/database/use-cases/check-encryption-health-use-case.test.js +0 -192
  71. package/database/use-cases/get-migration-status-use-case.test.js +0 -171
  72. package/database/use-cases/run-database-migration-use-case.test.js +0 -310
  73. package/database/use-cases/trigger-database-migration-use-case.test.js +0 -250
  74. package/database/utils/prisma-runner.test.js +0 -486
  75. package/encrypt/Cryptor.test.js +0 -144
  76. package/errors/base-error.test.js +0 -32
  77. package/errors/fetch-error.test.js +0 -79
  78. package/errors/halt-error.test.js +0 -11
  79. package/errors/validation-errors.test.js +0 -120
  80. package/handlers/auth-flow.integration.test.js +0 -147
  81. package/handlers/integration-event-dispatcher.test.js +0 -209
  82. package/handlers/routers/db-migration.test.js +0 -51
  83. package/handlers/routers/health.test.js +0 -210
  84. package/handlers/routers/integration-webhook-routers.test.js +0 -126
  85. package/handlers/use-cases/check-integrations-health-use-case.test.js +0 -125
  86. package/handlers/webhook-flow.integration.test.js +0 -356
  87. package/handlers/workers/db-migration.test.js +0 -50
  88. package/handlers/workers/integration-defined-workers.test.js +0 -184
  89. package/integrations/tests/integration-router-multi-auth.test.js +0 -369
  90. package/integrations/tests/use-cases/create-integration.test.js +0 -131
  91. package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -150
  92. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +0 -92
  93. package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -150
  94. package/integrations/tests/use-cases/get-integration-instance.test.js +0 -176
  95. package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -176
  96. package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
  97. package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
  98. package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
  99. package/integrations/tests/use-cases/update-integration.test.js +0 -141
  100. package/integrations/use-cases/create-process.test.js +0 -178
  101. package/integrations/use-cases/get-process.test.js +0 -190
  102. package/integrations/use-cases/load-integration-context-full.test.js +0 -329
  103. package/integrations/use-cases/load-integration-context.test.js +0 -114
  104. package/integrations/use-cases/update-process-metrics.test.js +0 -308
  105. package/integrations/use-cases/update-process-state.test.js +0 -256
  106. package/lambda/TimeoutCatcher.test.js +0 -68
  107. package/logs/logger.test.js +0 -76
  108. package/modules/module-hydration.test.js +0 -205
  109. package/modules/requester/requester.test.js +0 -28
  110. package/queues/queuer-util.test.js +0 -132
  111. package/user/tests/use-cases/create-individual-user.test.js +0 -24
  112. package/user/tests/use-cases/create-organization-user.test.js +0 -28
  113. package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
  114. package/user/tests/use-cases/get-user-from-adopter-jwt.test.js +0 -113
  115. package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
  116. package/user/tests/use-cases/get-user-from-x-frigg-headers.test.js +0 -346
  117. package/user/tests/use-cases/login-user.test.js +0 -220
  118. package/user/tests/user-password-encryption-isolation.test.js +0 -237
  119. package/user/tests/user-password-hashing.test.js +0 -235
  120. package/websocket/repositories/websocket-connection-repository.test.js +0 -227
@@ -1,439 +0,0 @@
1
- const { createEncryptionExtension } = require('./prisma-encryption-extension');
2
-
3
- describe('Prisma Encryption Extension', () => {
4
- let mockCryptor;
5
- let mockQuery;
6
-
7
- beforeEach(() => {
8
- // Mock Cryptor
9
- mockCryptor = {
10
- encrypt: jest
11
- .fn()
12
- .mockImplementation(
13
- (value) => `encrypted:${value}:iv:enckey`
14
- ),
15
- decrypt: jest
16
- .fn()
17
- .mockImplementation((value) => {
18
- const parts = value.split(':');
19
- return parts[1]; // Extract original value
20
- }),
21
- };
22
-
23
- // Mock Prisma query function
24
- mockQuery = jest.fn().mockImplementation((args) => args.mockResult);
25
- });
26
-
27
- describe('createEncryptionExtension', () => {
28
- it('should create extension with valid config', () => {
29
- const extension = createEncryptionExtension({
30
- cryptor: mockCryptor,
31
- enabled: true,
32
- });
33
-
34
- expect(extension).toBeDefined();
35
- expect(extension.name).toBe('frigg-field-encryption');
36
- expect(extension.query).toBeDefined();
37
- expect(extension.query.$allModels).toBeDefined();
38
- });
39
-
40
- it('should return no-op extension when disabled', () => {
41
- const extension = createEncryptionExtension({
42
- cryptor: mockCryptor,
43
- enabled: false,
44
- });
45
-
46
- // No-op extension is a function that returns its input
47
- const mockClient = { $extends: jest.fn() };
48
- const result = extension(mockClient);
49
-
50
- expect(result).toBe(mockClient);
51
- });
52
-
53
- it('should throw if cryptor not provided when enabled', () => {
54
- expect(() => {
55
- createEncryptionExtension({ enabled: true });
56
- }).toThrow('Cryptor instance required');
57
- });
58
-
59
- it('should not throw if cryptor not provided when disabled', () => {
60
- expect(() => {
61
- createEncryptionExtension({ enabled: false });
62
- }).not.toThrow();
63
- });
64
- });
65
-
66
- describe('Query Interceptors', () => {
67
- let extension;
68
- let handlers;
69
-
70
- beforeEach(() => {
71
- extension = createEncryptionExtension({
72
- cryptor: mockCryptor,
73
- enabled: true,
74
- });
75
- handlers = extension.query.$allModels;
76
- });
77
-
78
- describe('create', () => {
79
- it('should encrypt data before create', async () => {
80
- const args = {
81
- data: {
82
- data: { access_token: 'secret123' },
83
- },
84
- mockResult: {
85
- id: '1',
86
- data: { access_token: 'encrypted:secret123:iv:enckey' },
87
- },
88
- };
89
-
90
- await handlers.create({
91
- model: 'Credential',
92
- operation: 'create',
93
- args,
94
- query: mockQuery,
95
- });
96
-
97
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret123');
98
- expect(args.data.data.access_token).toBe(
99
- 'encrypted:secret123:iv:enckey'
100
- );
101
- });
102
-
103
- it('should decrypt result after create', async () => {
104
- const args = {
105
- data: { data: { access_token: 'secret123' } },
106
- mockResult: {
107
- id: '1',
108
- data: { access_token: 'encrypted:secret123:iv:enckey' },
109
- },
110
- };
111
-
112
- const result = await handlers.create({
113
- model: 'Credential',
114
- operation: 'create',
115
- args,
116
- query: mockQuery,
117
- });
118
-
119
- expect(mockCryptor.decrypt).toHaveBeenCalledWith(
120
- 'encrypted:secret123:iv:enckey'
121
- );
122
- expect(result.data.access_token).toBe('secret123');
123
- });
124
-
125
- it('should handle null result', async () => {
126
- const args = {
127
- data: { data: { access_token: 'secret123' } },
128
- mockResult: null,
129
- };
130
-
131
- const result = await handlers.create({
132
- model: 'Credential',
133
- operation: 'create',
134
- args,
135
- query: mockQuery,
136
- });
137
-
138
- expect(result).toBeNull();
139
- });
140
- });
141
-
142
- describe('createMany', () => {
143
- it('should encrypt array of data', async () => {
144
- const args = {
145
- data: [
146
- { data: { access_token: 'secret1' } },
147
- { data: { access_token: 'secret2' } },
148
- ],
149
- mockResult: { count: 2 },
150
- };
151
-
152
- await handlers.createMany({
153
- model: 'Credential',
154
- operation: 'createMany',
155
- args,
156
- query: mockQuery,
157
- });
158
-
159
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret1');
160
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret2');
161
- expect(args.data[0].data.access_token).toBe(
162
- 'encrypted:secret1:iv:enckey'
163
- );
164
- expect(args.data[1].data.access_token).toBe(
165
- 'encrypted:secret2:iv:enckey'
166
- );
167
- });
168
-
169
- it('should handle single object in createMany', async () => {
170
- const args = {
171
- data: { data: { access_token: 'secret' } },
172
- mockResult: { count: 1 },
173
- };
174
-
175
- await handlers.createMany({
176
- model: 'Credential',
177
- operation: 'createMany',
178
- args,
179
- query: mockQuery,
180
- });
181
-
182
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret');
183
- });
184
- });
185
-
186
- describe('update', () => {
187
- it('should encrypt update data', async () => {
188
- const args = {
189
- data: { data: { access_token: 'newsecret' } },
190
- mockResult: {
191
- id: '1',
192
- data: { access_token: 'encrypted:newsecret:iv:enckey' },
193
- },
194
- };
195
-
196
- await handlers.update({
197
- model: 'Credential',
198
- operation: 'update',
199
- args,
200
- query: mockQuery,
201
- });
202
-
203
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('newsecret');
204
- });
205
-
206
- it('should decrypt result after update', async () => {
207
- const args = {
208
- data: { data: { access_token: 'newsecret' } },
209
- mockResult: {
210
- id: '1',
211
- data: { access_token: 'encrypted:newsecret:iv:enckey' },
212
- },
213
- };
214
-
215
- const result = await handlers.update({
216
- model: 'Credential',
217
- operation: 'update',
218
- args,
219
- query: mockQuery,
220
- });
221
-
222
- expect(result.data.access_token).toBe('newsecret');
223
- });
224
- });
225
-
226
- describe('upsert', () => {
227
- it('should encrypt both create and update data', async () => {
228
- const args = {
229
- create: { data: { access_token: 'createsecret' } },
230
- update: { data: { access_token: 'updatesecret' } },
231
- mockResult: {
232
- id: '1',
233
- data: { access_token: 'encrypted:createsecret:iv:enckey' },
234
- },
235
- };
236
-
237
- await handlers.upsert({
238
- model: 'Credential',
239
- operation: 'upsert',
240
- args,
241
- query: mockQuery,
242
- });
243
-
244
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('createsecret');
245
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('updatesecret');
246
- });
247
- });
248
-
249
- describe('findUnique', () => {
250
- it('should decrypt result', async () => {
251
- const args = {
252
- where: { id: '1' },
253
- mockResult: {
254
- id: '1',
255
- data: { access_token: 'encrypted:secret:iv:enckey' },
256
- },
257
- };
258
-
259
- const result = await handlers.findUnique({
260
- model: 'Credential',
261
- operation: 'findUnique',
262
- args,
263
- query: mockQuery,
264
- });
265
-
266
- expect(mockCryptor.decrypt).toHaveBeenCalledWith(
267
- 'encrypted:secret:iv:enckey'
268
- );
269
- expect(result.data.access_token).toBe('secret');
270
- });
271
-
272
- it('should handle null result', async () => {
273
- const args = {
274
- where: { id: '999' },
275
- mockResult: null,
276
- };
277
-
278
- const result = await handlers.findUnique({
279
- model: 'Credential',
280
- operation: 'findUnique',
281
- args,
282
- query: mockQuery,
283
- });
284
-
285
- expect(result).toBeNull();
286
- expect(mockCryptor.decrypt).not.toHaveBeenCalled();
287
- });
288
- });
289
-
290
- describe('findMany', () => {
291
- it('should decrypt array of results', async () => {
292
- const args = {
293
- mockResult: [
294
- {
295
- id: '1',
296
- data: { access_token: 'encrypted:secret1:iv:enckey' },
297
- },
298
- {
299
- id: '2',
300
- data: { access_token: 'encrypted:secret2:iv:enckey' },
301
- },
302
- ],
303
- };
304
-
305
- const results = await handlers.findMany({
306
- model: 'Credential',
307
- operation: 'findMany',
308
- args,
309
- query: mockQuery,
310
- });
311
-
312
- expect(results).toHaveLength(2);
313
- expect(results[0].data.access_token).toBe('secret1');
314
- expect(results[1].data.access_token).toBe('secret2');
315
- });
316
-
317
- it('should handle empty array', async () => {
318
- const args = {
319
- mockResult: [],
320
- };
321
-
322
- const results = await handlers.findMany({
323
- model: 'Credential',
324
- operation: 'findMany',
325
- args,
326
- query: mockQuery,
327
- });
328
-
329
- expect(results).toEqual([]);
330
- });
331
- });
332
-
333
- describe('delete', () => {
334
- it('should decrypt deleted record', async () => {
335
- const args = {
336
- where: { id: '1' },
337
- mockResult: {
338
- id: '1',
339
- data: { access_token: 'encrypted:secret:iv:enckey' },
340
- },
341
- };
342
-
343
- const result = await handlers.delete({
344
- model: 'Credential',
345
- operation: 'delete',
346
- args,
347
- query: mockQuery,
348
- });
349
-
350
- expect(result.data.access_token).toBe('secret');
351
- });
352
- });
353
-
354
- describe('count', () => {
355
- it('should pass through without encryption', async () => {
356
- const args = {
357
- mockResult: { count: 5 },
358
- };
359
-
360
- const result = await handlers.count({
361
- model: 'Credential',
362
- operation: 'count',
363
- args,
364
- query: mockQuery,
365
- });
366
-
367
- expect(result).toEqual({ count: 5 });
368
- expect(mockCryptor.encrypt).not.toHaveBeenCalled();
369
- expect(mockCryptor.decrypt).not.toHaveBeenCalled();
370
- });
371
- });
372
-
373
- describe('Model without encrypted fields', () => {
374
- it('should pass through State model without encryption', async () => {
375
- const args = {
376
- data: { state: { some: 'data' } },
377
- mockResult: { id: '1', state: { some: 'data' } },
378
- };
379
-
380
- const result = await handlers.create({
381
- model: 'State',
382
- operation: 'create',
383
- args,
384
- query: mockQuery,
385
- });
386
-
387
- expect(result.state).toEqual({ some: 'data' });
388
- expect(mockCryptor.encrypt).not.toHaveBeenCalled();
389
- expect(mockCryptor.decrypt).not.toHaveBeenCalled();
390
- });
391
- });
392
- });
393
-
394
- describe('Integration with FieldEncryptionService', () => {
395
- it('should handle nested JSON paths correctly', async () => {
396
- const extension = createEncryptionExtension({
397
- cryptor: mockCryptor,
398
- enabled: true,
399
- });
400
- const handlers = extension.query.$allModels;
401
-
402
- const args = {
403
- data: {
404
- id: '123',
405
- data: {
406
- access_token: 'secret',
407
- refresh_token: 'refresh',
408
- other: 'public',
409
- },
410
- },
411
- mockResult: {
412
- id: '123',
413
- data: {
414
- access_token: 'encrypted:secret:iv:enckey',
415
- refresh_token: 'encrypted:refresh:iv:enckey',
416
- other: 'public',
417
- },
418
- },
419
- };
420
-
421
- const result = await handlers.create({
422
- model: 'Credential',
423
- operation: 'create',
424
- args,
425
- query: mockQuery,
426
- });
427
-
428
- // Verify encryption was called for encrypted fields
429
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('secret');
430
- expect(mockCryptor.encrypt).toHaveBeenCalledWith('refresh');
431
- // 'other' should not be encrypted
432
-
433
- // Verify decryption in result
434
- expect(result.data.access_token).toBe('secret');
435
- expect(result.data.refresh_token).toBe('refresh');
436
- expect(result.data.other).toBe('public');
437
- });
438
- });
439
- });
@@ -1,158 +0,0 @@
1
- /**
2
- * Tests for Migration Status Repository (S3)
3
- *
4
- * Tests S3-based storage for migration status tracking
5
- * (avoids chicken-and-egg dependency on User/Process tables)
6
- */
7
-
8
- const { MigrationStatusRepositoryS3 } = require('./migration-status-repository-s3');
9
-
10
- describe('MigrationStatusRepositoryS3', () => {
11
- let repository;
12
- let mockS3Client;
13
-
14
- beforeEach(() => {
15
- mockS3Client = {
16
- send: jest.fn(),
17
- };
18
- repository = new MigrationStatusRepositoryS3('test-bucket', mockS3Client);
19
- });
20
-
21
- describe('create()', () => {
22
- it('should create new migration status record in S3', async () => {
23
- const migrationData = {
24
- migrationId: 'migration-123',
25
- stage: 'dev',
26
- triggeredBy: 'admin',
27
- triggeredAt: '2025-10-19T12:00:00Z',
28
- };
29
-
30
- mockS3Client.send.mockResolvedValue({});
31
-
32
- const result = await repository.create(migrationData);
33
-
34
- expect(result.migrationId).toBe('migration-123');
35
- expect(result.state).toBe('INITIALIZING');
36
- expect(mockS3Client.send).toHaveBeenCalled();
37
- });
38
-
39
- it('should generate UUID if migrationId not provided', async () => {
40
- const migrationData = {
41
- stage: 'dev',
42
- triggeredBy: 'admin',
43
- triggeredAt: '2025-10-19T12:00:00Z',
44
- };
45
-
46
- mockS3Client.send.mockResolvedValue({});
47
-
48
- const result = await repository.create(migrationData);
49
-
50
- expect(result.migrationId).toMatch(/^[a-f0-9-]{36}$/); // UUID format
51
- expect(result.state).toBe('INITIALIZING');
52
- });
53
-
54
- it('should store status at correct S3 key', async () => {
55
- const migrationData = {
56
- migrationId: 'migration-123',
57
- stage: 'dev',
58
- };
59
-
60
- mockS3Client.send.mockResolvedValue({});
61
-
62
- await repository.create(migrationData);
63
-
64
- const putCommand = mockS3Client.send.mock.calls[0][0];
65
- expect(putCommand.input.Bucket).toBe('test-bucket');
66
- expect(putCommand.input.Key).toBe('migrations/dev/migration-123.json');
67
- });
68
- });
69
-
70
- describe('update()', () => {
71
- it('should update existing migration status', async () => {
72
- mockS3Client.send.mockResolvedValue({
73
- Body: {
74
- transformToString: () => JSON.stringify({
75
- migrationId: 'migration-123',
76
- state: 'INITIALIZING',
77
- progress: 0,
78
- }),
79
- },
80
- });
81
-
82
- const updateData = {
83
- migrationId: 'migration-123',
84
- stage: 'dev',
85
- state: 'RUNNING',
86
- progress: 50,
87
- };
88
-
89
- await repository.update(updateData);
90
-
91
- expect(mockS3Client.send).toHaveBeenCalledTimes(2); // GET then PUT
92
- });
93
-
94
- it('should merge updates with existing data', async () => {
95
- mockS3Client.send
96
- .mockResolvedValueOnce({
97
- Body: {
98
- transformToString: () => JSON.stringify({
99
- migrationId: 'migration-123',
100
- state: 'INITIALIZING',
101
- progress: 0,
102
- triggeredAt: '2025-10-19T12:00:00Z',
103
- }),
104
- },
105
- })
106
- .mockResolvedValueOnce({});
107
-
108
- await repository.update({
109
- migrationId: 'migration-123',
110
- stage: 'dev',
111
- state: 'COMPLETED',
112
- progress: 100,
113
- });
114
-
115
- const putCommand = mockS3Client.send.mock.calls[1][0];
116
- const storedData = JSON.parse(putCommand.input.Body);
117
- expect(storedData.triggeredAt).toBe('2025-10-19T12:00:00Z'); // Preserved
118
- expect(storedData.state).toBe('COMPLETED'); // Updated
119
- });
120
- });
121
-
122
- describe('get()', () => {
123
- it('should retrieve migration status from S3', async () => {
124
- const statusData = {
125
- migrationId: 'migration-123',
126
- state: 'COMPLETED',
127
- progress: 100,
128
- };
129
-
130
- mockS3Client.send.mockResolvedValue({
131
- Body: {
132
- transformToString: () => JSON.stringify(statusData),
133
- },
134
- });
135
-
136
- const result = await repository.get('migration-123', 'dev');
137
-
138
- expect(result).toEqual(statusData);
139
- expect(mockS3Client.send).toHaveBeenCalled();
140
- });
141
-
142
- it('should throw error if migration not found', async () => {
143
- mockS3Client.send.mockRejectedValue({ name: 'NoSuchKey' });
144
-
145
- await expect(repository.get('nonexistent', 'dev')).rejects.toThrow(
146
- 'Migration not found'
147
- );
148
- });
149
- });
150
-
151
- describe('S3 Key Generation', () => {
152
- it('should use consistent key format', () => {
153
- const key = repository._buildS3Key('migration-123', 'production');
154
- expect(key).toBe('migrations/production/migration-123.json');
155
- });
156
- });
157
- });
158
-