@adtrackify/at-service-common 3.18.8 → 3.18.9

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.
@@ -1,6 +1,9 @@
1
1
  import { unbundleRecords, unbundleMessage, isBundledEnvelope } from '../../clients/generic/sqs-unbundle';
2
2
  import { CompressionAlgorithm } from '../../clients/generic/sqs-bundled-client.types';
3
- import { compress, CompressionTimeoutError, CompressionOperation } from '../../utils/compression';
3
+ import { compress, CompressionTimeoutError, CompressionOperation, getGzipUncompressedSize, } from '../../utils/compression';
4
+ import { gzip } from 'node:zlib';
5
+ import { promisify } from 'node:util';
6
+ const gzipAsync = promisify(gzip);
4
7
  jest.mock('../../helpers/logging-helper', () => ({
5
8
  Logger: {
6
9
  debug: jest.fn(),
@@ -39,6 +42,18 @@ async function makeCompressedEnvelope(items, level = 3) {
39
42
  p: base64,
40
43
  };
41
44
  }
45
+ async function makeGzipCompressedEnvelope(items) {
46
+ const json = JSON.stringify(items);
47
+ const compressed = await gzipAsync(Buffer.from(json, 'utf8'));
48
+ const base64 = compressed.toString('base64');
49
+ return {
50
+ v: 1,
51
+ c: CompressionAlgorithm.GZIP,
52
+ n: items.length,
53
+ s: json.length,
54
+ p: base64,
55
+ };
56
+ }
42
57
  describe('isBundledEnvelope', () => {
43
58
  it('returns true for valid bundled envelope with NONE compression', () => {
44
59
  const envelope = makeBundledEnvelope([makeTestEvent('1')]);
@@ -61,8 +76,12 @@ describe('isBundledEnvelope', () => {
61
76
  const envelope = { v: 2, c: 'none', n: 1, p: [] };
62
77
  expect(isBundledEnvelope(envelope)).toBe(false);
63
78
  });
79
+ it('returns true for valid bundled envelope with GZIP compression', async () => {
80
+ const envelope = await makeGzipCompressedEnvelope([makeTestEvent('1')]);
81
+ expect(isBundledEnvelope(envelope)).toBe(true);
82
+ });
64
83
  it('returns false for invalid compression algorithm', () => {
65
- const envelope = { v: 1, c: 'gzip', n: 1, p: [] };
84
+ const envelope = { v: 1, c: 'lz4', n: 1, p: [] };
66
85
  expect(isBundledEnvelope(envelope)).toBe(false);
67
86
  });
68
87
  it('returns false for missing count', () => {
@@ -112,6 +131,27 @@ describe('unbundleMessage', () => {
112
131
  const envelope = { v: 1, c: 'zstd', n: 1, p: [] };
113
132
  await expect(unbundleMessage(envelope)).rejects.toThrow('Invalid bundle: zstd compression but payload is not a string');
114
133
  });
134
+ it('unbundles GZIP compressed envelope', async () => {
135
+ const items = [makeTestEvent('1'), makeTestEvent('2'), makeTestEvent('3')];
136
+ const envelope = await makeGzipCompressedEnvelope(items);
137
+ const result = await unbundleMessage(envelope);
138
+ expect(result).toHaveLength(3);
139
+ expect(result[0].eventId).toBe('1');
140
+ expect(result[1].eventId).toBe('2');
141
+ expect(result[2].eventId).toBe('3');
142
+ });
143
+ it('unbundles large batches with GZIP compression', async () => {
144
+ const items = Array.from({ length: 500 }, (_, i) => makeTestEvent(`event-${i}`, `event_${i}`, i * 10));
145
+ const envelope = await makeGzipCompressedEnvelope(items);
146
+ const result = await unbundleMessage(envelope);
147
+ expect(result).toHaveLength(500);
148
+ expect(result[0].eventId).toBe('event-0');
149
+ expect(result[499].eventId).toBe('event-499');
150
+ });
151
+ it('throws on invalid payload type for GZIP compression', async () => {
152
+ const envelope = { v: 1, c: 'gzip', n: 1, p: [] };
153
+ await expect(unbundleMessage(envelope)).rejects.toThrow('Invalid bundle: gzip compression but payload is not a string');
154
+ });
115
155
  });
116
156
  describe('unbundleRecords', () => {
117
157
  describe('legacy (non-bundled) messages', () => {
@@ -201,6 +241,37 @@ describe('unbundleRecords', () => {
201
241
  expect(stats.totalItems).toBe(500);
202
242
  });
203
243
  });
244
+ describe('bundled messages (GZIP compression - CF Worker)', () => {
245
+ it('unbundles GZIP compressed envelope', async () => {
246
+ const items = [makeTestEvent('g1'), makeTestEvent('g2')];
247
+ const envelope = await makeGzipCompressedEnvelope(items);
248
+ const record = makeSqsRecord({ messageBody: envelope });
249
+ const { items: result, stats } = await unbundleRecords([record]);
250
+ expect(result).toHaveLength(2);
251
+ expect(result[0].eventId).toBe('g1');
252
+ expect(result[1].eventId).toBe('g2');
253
+ expect(stats.bundledSqsRecords).toBe(1);
254
+ });
255
+ it('handles large GZIP compressed bundles (500 items)', async () => {
256
+ const items = Array.from({ length: 500 }, (_, i) => makeTestEvent(`item-${i}`));
257
+ const envelope = await makeGzipCompressedEnvelope(items);
258
+ const record = makeSqsRecord({ messageBody: envelope });
259
+ const { items: result, stats } = await unbundleRecords([record]);
260
+ expect(result).toHaveLength(500);
261
+ expect(stats.bundledSqsRecords).toBe(1);
262
+ expect(stats.totalItems).toBe(500);
263
+ });
264
+ it('maps all items from GZIP bundle to same messageId', async () => {
265
+ const items = [makeTestEvent('x1'), makeTestEvent('x2'), makeTestEvent('x3')];
266
+ const envelope = await makeGzipCompressedEnvelope(items);
267
+ const record = makeSqsRecord({ messageBody: envelope }, 'gzip-bundle-msg-1');
268
+ const { items: result, recordMap } = await unbundleRecords([record]);
269
+ expect(result).toHaveLength(3);
270
+ expect(recordMap.get(result[0])).toBe('gzip-bundle-msg-1');
271
+ expect(recordMap.get(result[1])).toBe('gzip-bundle-msg-1');
272
+ expect(recordMap.get(result[2])).toBe('gzip-bundle-msg-1');
273
+ });
274
+ });
204
275
  describe('mixed message formats', () => {
205
276
  it('handles mix of legacy and bundled messages', async () => {
206
277
  const legacyEvent = makeTestEvent('legacy-1');
@@ -233,6 +304,29 @@ describe('unbundleRecords', () => {
233
304
  expect(stats.bundledSqsRecords).toBe(2);
234
305
  expect(stats.totalItems).toBe(3);
235
306
  });
307
+ it('handles mix of ZSTD, GZIP, and legacy messages', async () => {
308
+ const legacyEvent = makeTestEvent('legacy-1');
309
+ const zstdItems = [makeTestEvent('zstd-1'), makeTestEvent('zstd-2')];
310
+ const gzipItems = [makeTestEvent('gzip-1'), makeTestEvent('gzip-2'), makeTestEvent('gzip-3')];
311
+ const zstdEnvelope = await makeCompressedEnvelope(zstdItems);
312
+ const gzipEnvelope = await makeGzipCompressedEnvelope(gzipItems);
313
+ const records = [
314
+ makeSqsRecord(legacyEvent, 'msg-legacy'),
315
+ makeSqsRecord({ messageBody: zstdEnvelope }, 'msg-zstd'),
316
+ makeSqsRecord({ messageBody: gzipEnvelope }, 'msg-gzip'),
317
+ ];
318
+ const { items, stats, recordMap } = await unbundleRecords(records);
319
+ expect(items).toHaveLength(6);
320
+ expect(stats.legacySqsRecords).toBe(1);
321
+ expect(stats.bundledSqsRecords).toBe(2);
322
+ expect(stats.totalItems).toBe(6);
323
+ expect(recordMap.get(items[0])).toBe('msg-legacy');
324
+ expect(recordMap.get(items[1])).toBe('msg-zstd');
325
+ expect(recordMap.get(items[2])).toBe('msg-zstd');
326
+ expect(recordMap.get(items[3])).toBe('msg-gzip');
327
+ expect(recordMap.get(items[4])).toBe('msg-gzip');
328
+ expect(recordMap.get(items[5])).toBe('msg-gzip');
329
+ });
236
330
  });
237
331
  describe('error handling', () => {
238
332
  it('returns failed messageId for invalid JSON', async () => {
@@ -244,7 +338,7 @@ describe('unbundleRecords', () => {
244
338
  expect(stats.failedRecords).toBe(1);
245
339
  expect(stats.legacySqsRecords).toBe(1);
246
340
  });
247
- it('returns failed messageId for corrupt compressed data', async () => {
341
+ it('returns failed messageId for corrupt ZSTD compressed data', async () => {
248
342
  const corruptEnvelope = {
249
343
  v: 1,
250
344
  c: CompressionAlgorithm.ZSTD,
@@ -261,6 +355,23 @@ describe('unbundleRecords', () => {
261
355
  expect(failedMessageIds).toContain('msg-corrupt');
262
356
  expect(stats.failedRecords).toBe(1);
263
357
  });
358
+ it('returns failed messageId for corrupt GZIP compressed data', async () => {
359
+ const corruptEnvelope = {
360
+ v: 1,
361
+ c: CompressionAlgorithm.GZIP,
362
+ n: 10,
363
+ p: 'dGhpcyBpcyBub3QgdmFsaWQgZ3ppcA==',
364
+ };
365
+ const records = [
366
+ makeSqsRecord({ messageBody: corruptEnvelope }, 'msg-corrupt-gzip'),
367
+ makeSqsRecord(makeTestEvent('valid'), 'msg-valid'),
368
+ ];
369
+ const { items, failedMessageIds, stats } = await unbundleRecords(records);
370
+ expect(items).toHaveLength(1);
371
+ expect(items[0].eventId).toBe('valid');
372
+ expect(failedMessageIds).toContain('msg-corrupt-gzip');
373
+ expect(stats.failedRecords).toBe(1);
374
+ });
264
375
  it('returns empty result for all failed records', async () => {
265
376
  const records = [makeSqsRecord('invalid-1', 'msg-1'), makeSqsRecord('invalid-2', 'msg-2')];
266
377
  const { items, failedMessageIds, stats } = await unbundleRecords(records);
@@ -276,7 +387,7 @@ describe('unbundleRecords', () => {
276
387
  expect(stats.totalRecords).toBe(0);
277
388
  expect(stats.totalItems).toBe(0);
278
389
  });
279
- it('rejects payload exceeding max decompressed size', async () => {
390
+ it('rejects ZSTD payload exceeding max decompressed size', async () => {
280
391
  const largeItems = Array.from({ length: 1000 }, (_, i) => ({
281
392
  id: `item-${i}`,
282
393
  data: 'x'.repeat(1000),
@@ -289,6 +400,19 @@ describe('unbundleRecords', () => {
289
400
  expect(failedMessageIds).toContain('msg-too-large');
290
401
  expect(stats.failedRecords).toBe(1);
291
402
  });
403
+ it('rejects GZIP payload exceeding max decompressed size (post-decompress check)', async () => {
404
+ const largeItems = Array.from({ length: 1000 }, (_, i) => ({
405
+ id: `item-${i}`,
406
+ data: 'x'.repeat(1000),
407
+ }));
408
+ const envelope = await makeGzipCompressedEnvelope(largeItems);
409
+ const record = makeSqsRecord({ messageBody: envelope }, 'msg-too-large-gzip');
410
+ const { failedMessageIds, stats } = await unbundleRecords([record], {
411
+ maxDecompressedSizeBytes: 100 * 1024,
412
+ });
413
+ expect(failedMessageIds).toContain('msg-too-large-gzip');
414
+ expect(stats.failedRecords).toBe(1);
415
+ });
292
416
  it('fails on invalid base64 in compressed payload', async () => {
293
417
  const badEnvelope = {
294
418
  v: 1,
@@ -326,7 +450,7 @@ describe('unbundleRecords', () => {
326
450
  });
327
451
  });
328
452
  describe('timeout protection', () => {
329
- it('respects timeoutMs option', async () => {
453
+ it('respects timeoutMs option for ZSTD', async () => {
330
454
  const items = [makeTestEvent('1')];
331
455
  const envelope = await makeCompressedEnvelope(items);
332
456
  const record = makeSqsRecord({ messageBody: envelope });
@@ -336,6 +460,16 @@ describe('unbundleRecords', () => {
336
460
  expect(result).toHaveLength(1);
337
461
  expect(failedMessageIds).toHaveLength(0);
338
462
  });
463
+ it('respects timeoutMs option for GZIP', async () => {
464
+ const items = [makeTestEvent('1')];
465
+ const envelope = await makeGzipCompressedEnvelope(items);
466
+ const record = makeSqsRecord({ messageBody: envelope });
467
+ const { items: result, failedMessageIds } = await unbundleRecords([record], {
468
+ timeoutMs: 5000,
469
+ });
470
+ expect(result).toHaveLength(1);
471
+ expect(failedMessageIds).toHaveLength(0);
472
+ });
339
473
  it('passes timeoutMs through to decompression', async () => {
340
474
  const items = [makeTestEvent('1')];
341
475
  const envelope = await makeCompressedEnvelope(items);
@@ -350,7 +484,7 @@ describe('unbundleRecords', () => {
350
484
  expect(error.timeoutMs).toBe(5000);
351
485
  expect(error.message).toContain('timed out after 5000ms');
352
486
  });
353
- it('includes timeout info in failure logging', async () => {
487
+ it('includes timeout info in failure logging for ZSTD', async () => {
354
488
  const corruptEnvelope = {
355
489
  v: 1,
356
490
  c: CompressionAlgorithm.ZSTD,
@@ -364,6 +498,20 @@ describe('unbundleRecords', () => {
364
498
  });
365
499
  expect(failedMessageIds).toContain('msg-timeout-test');
366
500
  });
501
+ it('includes timeout info in failure logging for GZIP', async () => {
502
+ const corruptEnvelope = {
503
+ v: 1,
504
+ c: CompressionAlgorithm.GZIP,
505
+ n: 1,
506
+ s: 100,
507
+ p: 'dGhpcyBpcyBub3QgdmFsaWQgZ3ppcA==',
508
+ };
509
+ const record = makeSqsRecord({ messageBody: corruptEnvelope }, 'msg-gzip-timeout-test');
510
+ const { failedMessageIds } = await unbundleRecords([record], {
511
+ timeoutMs: 5000,
512
+ });
513
+ expect(failedMessageIds).toContain('msg-gzip-timeout-test');
514
+ });
367
515
  });
368
516
  describe('end-to-end: producer bundle → consumer unbundle', () => {
369
517
  it('simulates full bundle → unbundle flow with compression', async () => {
@@ -772,5 +920,308 @@ describe('unbundleRecords', () => {
772
920
  expect(stats.totalItems).toBe(0);
773
921
  });
774
922
  });
923
+ describe('GZIP comprehensive tests', () => {
924
+ describe('GZIP pre-decompression bomb protection', () => {
925
+ it('rejects GZIP with fake ISIZE claiming huge size BEFORE decompression starts', async () => {
926
+ const gzipHeader = Buffer.from([0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03]);
927
+ const emptyDeflate = Buffer.from([0x03, 0x00]);
928
+ const crc32 = Buffer.from([0x00, 0x00, 0x00, 0x00]);
929
+ const fakeIsize = Buffer.alloc(4);
930
+ fakeIsize.writeUInt32LE(100 * 1024 * 1024, 0);
931
+ const fakeGzip = Buffer.concat([gzipHeader, emptyDeflate, crc32, fakeIsize]);
932
+ const base64 = fakeGzip.toString('base64');
933
+ const bombEnvelope = {
934
+ v: 1,
935
+ c: CompressionAlgorithm.GZIP,
936
+ n: 1,
937
+ s: 100,
938
+ p: base64,
939
+ };
940
+ const record = makeSqsRecord({ messageBody: bombEnvelope }, 'msg-gzip-bomb');
941
+ const { failedMessageIds, stats } = await unbundleRecords([record], {
942
+ maxDecompressedSizeBytes: 10 * 1024 * 1024,
943
+ });
944
+ expect(failedMessageIds).toContain('msg-gzip-bomb');
945
+ expect(stats.failedRecords).toBe(1);
946
+ });
947
+ it('allows GZIP with ISIZE within limit', async () => {
948
+ const items = [makeTestEvent('valid-1')];
949
+ const envelope = await makeGzipCompressedEnvelope(items);
950
+ const record = makeSqsRecord({ messageBody: envelope }, 'msg-gzip-valid');
951
+ const { items: result, failedMessageIds } = await unbundleRecords([record], {
952
+ maxDecompressedSizeBytes: 10 * 1024 * 1024,
953
+ });
954
+ expect(result).toHaveLength(1);
955
+ expect(failedMessageIds).toHaveLength(0);
956
+ });
957
+ it('pre-check catches bomb even when ISIZE is exactly at limit + 1', async () => {
958
+ const limit = 1000;
959
+ const gzipHeader = Buffer.from([0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03]);
960
+ const emptyDeflate = Buffer.from([0x03, 0x00]);
961
+ const crc32 = Buffer.from([0x00, 0x00, 0x00, 0x00]);
962
+ const fakeIsize = Buffer.alloc(4);
963
+ fakeIsize.writeUInt32LE(limit + 1, 0);
964
+ const fakeGzip = Buffer.concat([gzipHeader, emptyDeflate, crc32, fakeIsize]);
965
+ const base64 = fakeGzip.toString('base64');
966
+ const bombEnvelope = {
967
+ v: 1,
968
+ c: CompressionAlgorithm.GZIP,
969
+ n: 1,
970
+ s: 100,
971
+ p: base64,
972
+ };
973
+ const record = makeSqsRecord({ messageBody: bombEnvelope }, 'msg-boundary-bomb');
974
+ const { failedMessageIds } = await unbundleRecords([record], {
975
+ maxDecompressedSizeBytes: limit,
976
+ });
977
+ expect(failedMessageIds).toContain('msg-boundary-bomb');
978
+ });
979
+ });
980
+ describe('empty GZIP bundle', () => {
981
+ it('unbundles empty GZIP compressed array', async () => {
982
+ const envelope = await makeGzipCompressedEnvelope([]);
983
+ const record = makeSqsRecord({ messageBody: envelope }, 'msg-empty-gzip');
984
+ const { items, stats } = await unbundleRecords([record]);
985
+ expect(items).toHaveLength(0);
986
+ expect(stats.bundledSqsRecords).toBe(1);
987
+ expect(stats.totalItems).toBe(0);
988
+ });
989
+ });
990
+ describe('Unicode characters in GZIP payload', () => {
991
+ it('preserves emoji through GZIP compression', async () => {
992
+ const items = [
993
+ { eventId: 'emoji-1', content: '🎉🚀💻🌍✨' },
994
+ { eventId: 'emoji-2', content: '👋🏽👨‍👩‍👧‍👦🏳️‍🌈' },
995
+ ];
996
+ const envelope = await makeGzipCompressedEnvelope(items);
997
+ const record = makeSqsRecord({ messageBody: envelope });
998
+ const { items: result } = await unbundleRecords([record]);
999
+ expect(result).toHaveLength(2);
1000
+ expect(result[0].content).toBe('🎉🚀💻🌍✨');
1001
+ expect(result[1].content).toBe('👋🏽👨‍👩‍👧‍👦🏳️‍🌈');
1002
+ });
1003
+ it('preserves CJK characters through GZIP compression', async () => {
1004
+ const items = [
1005
+ { eventId: 'cjk-1', content: '中文测试:欢迎使用' },
1006
+ { eventId: 'cjk-2', content: 'こんにちは世界 ひらがな カタカナ' },
1007
+ { eventId: 'cjk-3', content: '안녕하세요 세계' },
1008
+ ];
1009
+ const envelope = await makeGzipCompressedEnvelope(items);
1010
+ const record = makeSqsRecord({ messageBody: envelope });
1011
+ const { items: result } = await unbundleRecords([record]);
1012
+ expect(result).toHaveLength(3);
1013
+ expect(result[0].content).toBe('中文测试:欢迎使用');
1014
+ expect(result[1].content).toBe('こんにちは世界 ひらがな カタカナ');
1015
+ expect(result[2].content).toBe('안녕하세요 세계');
1016
+ });
1017
+ it('preserves special characters and mixed scripts through GZIP compression', async () => {
1018
+ const items = [
1019
+ { eventId: 'special-1', content: '∑∫∂∇ε → ∞ × ∈ ⊆ ∀∃' },
1020
+ { eventId: 'special-2', content: 'العربية: مرحبا بكم' },
1021
+ { eventId: 'mixed', content: 'Hello 世界 🌏 مرحبا Привет' },
1022
+ { eventId: 'combining', content: 'e\u0301 n\u0303 c\u0327' },
1023
+ ];
1024
+ const envelope = await makeGzipCompressedEnvelope(items);
1025
+ const record = makeSqsRecord({ messageBody: envelope });
1026
+ const { items: result } = await unbundleRecords([record]);
1027
+ expect(result).toHaveLength(4);
1028
+ for (let i = 0; i < items.length; i++) {
1029
+ expect(result[i].content).toBe(items[i].content);
1030
+ const originalBytes = Buffer.from(items[i].content, 'utf8');
1031
+ const recoveredBytes = Buffer.from(result[i].content, 'utf8');
1032
+ expect(recoveredBytes.equals(originalBytes)).toBe(true);
1033
+ }
1034
+ });
1035
+ });
1036
+ describe('GZIP with invalid magic bytes', () => {
1037
+ it('fails gracefully for buffer with wrong magic bytes', async () => {
1038
+ const wrongMagic = Buffer.from([0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03]);
1039
+ const filler = Buffer.alloc(10);
1040
+ const invalidGzip = Buffer.concat([wrongMagic, filler]);
1041
+ const base64 = invalidGzip.toString('base64');
1042
+ const badEnvelope = {
1043
+ v: 1,
1044
+ c: CompressionAlgorithm.GZIP,
1045
+ n: 1,
1046
+ s: 100,
1047
+ p: base64,
1048
+ };
1049
+ const record = makeSqsRecord({ messageBody: badEnvelope }, 'msg-wrong-magic');
1050
+ const { failedMessageIds } = await unbundleRecords([record]);
1051
+ expect(failedMessageIds).toContain('msg-wrong-magic');
1052
+ });
1053
+ it('fails gracefully for buffer starting with valid magic but corrupted data', async () => {
1054
+ const validMagic = Buffer.from([0x1f, 0x8b]);
1055
+ const garbage = Buffer.from('this is not valid gzip data after magic');
1056
+ const invalidGzip = Buffer.concat([validMagic, garbage]);
1057
+ const base64 = invalidGzip.toString('base64');
1058
+ const badEnvelope = {
1059
+ v: 1,
1060
+ c: CompressionAlgorithm.GZIP,
1061
+ n: 1,
1062
+ s: 100,
1063
+ p: base64,
1064
+ };
1065
+ const record = makeSqsRecord({ messageBody: badEnvelope }, 'msg-corrupt-gzip');
1066
+ const { failedMessageIds } = await unbundleRecords([record]);
1067
+ expect(failedMessageIds).toContain('msg-corrupt-gzip');
1068
+ });
1069
+ });
1070
+ describe('GZIP minimum size edge cases', () => {
1071
+ it('handles buffer exactly 18 bytes (minimum valid)', async () => {
1072
+ const minBuffer = Buffer.alloc(18);
1073
+ minBuffer[0] = 0x1f;
1074
+ minBuffer[1] = 0x8b;
1075
+ const size = getGzipUncompressedSize(minBuffer);
1076
+ expect(size).toBe(0);
1077
+ });
1078
+ it('returns undefined for buffer exactly 17 bytes (too small)', async () => {
1079
+ const tooSmall = Buffer.alloc(17);
1080
+ tooSmall[0] = 0x1f;
1081
+ tooSmall[1] = 0x8b;
1082
+ const size = getGzipUncompressedSize(tooSmall);
1083
+ expect(size).toBeUndefined();
1084
+ });
1085
+ it('fails gracefully for 17-byte GZIP envelope', async () => {
1086
+ const tooSmall = Buffer.alloc(17);
1087
+ tooSmall[0] = 0x1f;
1088
+ tooSmall[1] = 0x8b;
1089
+ const base64 = tooSmall.toString('base64');
1090
+ const badEnvelope = {
1091
+ v: 1,
1092
+ c: CompressionAlgorithm.GZIP,
1093
+ n: 1,
1094
+ s: 100,
1095
+ p: base64,
1096
+ };
1097
+ const record = makeSqsRecord({ messageBody: badEnvelope }, 'msg-too-small');
1098
+ const { failedMessageIds } = await unbundleRecords([record]);
1099
+ expect(failedMessageIds).toContain('msg-too-small');
1100
+ });
1101
+ });
1102
+ describe('GZIP timeout protection', () => {
1103
+ it('CompressionTimeoutError has correct properties', () => {
1104
+ const error = new CompressionTimeoutError(CompressionOperation.DECOMPRESS, 100);
1105
+ expect(error).toBeInstanceOf(Error);
1106
+ expect(error.name).toBe('CompressionTimeoutError');
1107
+ expect(error.operation).toBe('decompress');
1108
+ expect(error.timeoutMs).toBe(100);
1109
+ expect(error.message).toContain('timed out after 100ms');
1110
+ });
1111
+ it('GZIP decompression respects timeout option', async () => {
1112
+ const items = [makeTestEvent('timeout-test')];
1113
+ const envelope = await makeGzipCompressedEnvelope(items);
1114
+ const record = makeSqsRecord({ messageBody: envelope });
1115
+ const { items: result, failedMessageIds } = await unbundleRecords([record], {
1116
+ timeoutMs: 10000,
1117
+ });
1118
+ expect(result).toHaveLength(1);
1119
+ expect(failedMessageIds).toHaveLength(0);
1120
+ });
1121
+ });
1122
+ describe('large GZIP near SQS limit', () => {
1123
+ it('decompresses large GZIP bundle near 256KB compressed', async () => {
1124
+ const items = Array.from({ length: 200 }, (_, i) => ({
1125
+ eventId: `large-${i}`,
1126
+ name: 'page_view',
1127
+ data: `repeated-content-for-compression-${i}-`.repeat(50),
1128
+ value: i * 1.5,
1129
+ }));
1130
+ const envelope = await makeGzipCompressedEnvelope(items);
1131
+ const compressedSize = Buffer.byteLength(JSON.stringify(envelope.p), 'utf8');
1132
+ console.log(`Large GZIP test: compressed payload ~${Math.round(compressedSize / 1024)}KB`);
1133
+ const record = makeSqsRecord({ messageBody: envelope }, 'msg-large-gzip');
1134
+ const { items: result, failedMessageIds } = await unbundleRecords([record]);
1135
+ expect(failedMessageIds).toHaveLength(0);
1136
+ expect(result).toHaveLength(200);
1137
+ expect(result[0].eventId).toBe('large-0');
1138
+ expect(result[199].eventId).toBe('large-199');
1139
+ });
1140
+ it('handles GZIP with maximum decompressed size exactly at limit', async () => {
1141
+ const items = [{ eventId: 'boundary', data: 'x'.repeat(5000) }];
1142
+ const envelope = await makeGzipCompressedEnvelope(items);
1143
+ const record = makeSqsRecord({ messageBody: envelope });
1144
+ const { items: result, failedMessageIds } = await unbundleRecords([record], {
1145
+ maxDecompressedSizeBytes: 10 * 1024,
1146
+ });
1147
+ expect(failedMessageIds).toHaveLength(0);
1148
+ expect(result).toHaveLength(1);
1149
+ });
1150
+ });
1151
+ });
1152
+ });
1153
+ describe('getGzipUncompressedSize', () => {
1154
+ it('returns correct size for valid GZIP', async () => {
1155
+ const originalData = 'Hello, World! This is a test.';
1156
+ const compressed = await gzipAsync(Buffer.from(originalData, 'utf8'));
1157
+ const size = getGzipUncompressedSize(compressed);
1158
+ expect(size).toBe(Buffer.byteLength(originalData, 'utf8'));
1159
+ });
1160
+ it('returns correct size for larger payload', async () => {
1161
+ const originalData = 'x'.repeat(10000);
1162
+ const compressed = await gzipAsync(Buffer.from(originalData, 'utf8'));
1163
+ const size = getGzipUncompressedSize(compressed);
1164
+ expect(size).toBe(10000);
1165
+ });
1166
+ it('returns undefined for buffer too small', () => {
1167
+ const tooSmall = Buffer.alloc(17);
1168
+ tooSmall[0] = 0x1f;
1169
+ tooSmall[1] = 0x8b;
1170
+ expect(getGzipUncompressedSize(tooSmall)).toBeUndefined();
1171
+ });
1172
+ it('returns undefined for wrong magic bytes', () => {
1173
+ const wrongMagic = Buffer.alloc(20);
1174
+ wrongMagic[0] = 0x00;
1175
+ wrongMagic[1] = 0x00;
1176
+ expect(getGzipUncompressedSize(wrongMagic)).toBeUndefined();
1177
+ });
1178
+ it('returns undefined for empty buffer', () => {
1179
+ expect(getGzipUncompressedSize(Buffer.alloc(0))).toBeUndefined();
1180
+ });
1181
+ it('returns undefined for single byte buffer', () => {
1182
+ expect(getGzipUncompressedSize(Buffer.from([0x1f]))).toBeUndefined();
1183
+ });
1184
+ it('returns undefined for buffer with only magic bytes', () => {
1185
+ expect(getGzipUncompressedSize(Buffer.from([0x1f, 0x8b]))).toBeUndefined();
1186
+ });
1187
+ it('returns 0 for GZIP with ISIZE = 0', () => {
1188
+ const buffer = Buffer.alloc(18);
1189
+ buffer[0] = 0x1f;
1190
+ buffer[1] = 0x8b;
1191
+ const size = getGzipUncompressedSize(buffer);
1192
+ expect(size).toBe(0);
1193
+ });
1194
+ it('reads ISIZE correctly as little-endian', async () => {
1195
+ const buffer = Buffer.alloc(18);
1196
+ buffer[0] = 0x1f;
1197
+ buffer[1] = 0x8b;
1198
+ buffer.writeUInt32LE(0x12345678, 14);
1199
+ const size = getGzipUncompressedSize(buffer);
1200
+ expect(size).toBe(0x12345678);
1201
+ });
1202
+ it('handles maximum 32-bit ISIZE value', () => {
1203
+ const buffer = Buffer.alloc(18);
1204
+ buffer[0] = 0x1f;
1205
+ buffer[1] = 0x8b;
1206
+ buffer.writeUInt32LE(0xffffffff, 14);
1207
+ const size = getGzipUncompressedSize(buffer);
1208
+ expect(size).toBe(0xffffffff);
1209
+ });
1210
+ it('correctly extracts size from real GZIP data', async () => {
1211
+ const testSizes = [1, 10, 100, 1000, 50000];
1212
+ for (const testSize of testSizes) {
1213
+ const originalData = 'a'.repeat(testSize);
1214
+ const compressed = await gzipAsync(Buffer.from(originalData, 'utf8'));
1215
+ const size = getGzipUncompressedSize(compressed);
1216
+ expect(size).toBe(testSize);
1217
+ }
1218
+ });
1219
+ it('handles JSON array compression correctly', async () => {
1220
+ const items = [{ id: 1 }, { id: 2 }, { id: 3 }];
1221
+ const json = JSON.stringify(items);
1222
+ const compressed = await gzipAsync(Buffer.from(json, 'utf8'));
1223
+ const size = getGzipUncompressedSize(compressed);
1224
+ expect(size).toBe(Buffer.byteLength(json, 'utf8'));
1225
+ });
775
1226
  });
776
1227
  //# sourceMappingURL=sqs-unbundle.spec.js.map