From 518972636a8710f1ab6b0eb82ae49799775f22d8 Mon Sep 17 00:00:00 2001 From: Nev Wylie <54870357+MSNev@users.noreply.github.com> Date: Mon, 5 May 2025 10:01:21 -0700 Subject: [PATCH] Add additional SendPostManager tests --- common/config/rush/npm-shrinkwrap.json | 13 - .../Tests/Unit/src/SendPostManager.Tests.ts | 269 ++++++++++++++++++ .../Tests/Unit/src/aiunittests.ts | 2 + .../src/JavaScriptSDK/SenderPostManager.ts | 3 +- 4 files changed, 273 insertions(+), 14 deletions(-) create mode 100644 shared/AppInsightsCore/Tests/Unit/src/SendPostManager.Tests.ts diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index 1862f1ff3..e350d46a7 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -3529,19 +3529,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/shared/AppInsightsCore/Tests/Unit/src/SendPostManager.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/SendPostManager.Tests.ts new file mode 100644 index 000000000..fad029b30 --- /dev/null +++ b/shared/AppInsightsCore/Tests/Unit/src/SendPostManager.Tests.ts @@ -0,0 +1,269 @@ +import { Assert, AITestClass } from "@microsoft/ai-test-framework"; +import { _eInternalMessageId } from "../../../src/JavaScriptSDK.Enums/LoggingEnums"; +import { _InternalLogMessage } from "../../../src/JavaScriptSDK/DiagnosticLogger"; +import { SenderPostManager } from "../../../src/JavaScriptSDK/SenderPostManager"; +import { IPayloadData } from "../../../src/JavaScriptSDK.Interfaces/IXHROverride"; +import { getInst, isFunction, mathRandom } from "@nevware21/ts-utils"; +import { createPromise, doAwaitResponse } from "@nevware21/ts-async"; + +export class SendPostManagerTests extends AITestClass { + private _sender: SenderPostManager; + private _isCompressionSupported: boolean; + + public testInitialize() { + super.testInitialize(); + this._sender = new SenderPostManager(); + + // Check if CompressionStream is supported in this environment + const csStream = getInst("CompressionStream"); + this._isCompressionSupported = isFunction(csStream); + } + + public testCleanup() { + super.testCleanup(); + this._sender = null; + } + + public registerTests() { + this.testCase({ + name: "preparePayload: compression disabled", + test: () => { + const originalData = "This is test data for compression disabled test"; + const payload: IPayloadData = { + urlString: "https://test.com", + data: originalData, + headers: {} + }; + + return createPromise((resolve) => { + let callbackPayload: IPayloadData = null; + const callback = (processedPayload: IPayloadData) => { + callbackPayload = processedPayload; + resolve(); + }; + + // Compression disabled (zipPayload = false) + this._sender.preparePayload(callback, false, payload, false); + + Assert.ok(callbackPayload, "Callback should be called with payload"); + Assert.equal(callbackPayload.data, originalData, "Data should remain unchanged when compression is disabled"); + Assert.ok(!callbackPayload.headers["Content-Encoding"], "Content-Encoding header should not be set"); + }); + } + }); + + this.testCase({ + name: "preparePayload: isSync should bypass compression", + test: () => { + const originalData = "This is test data for isSync test"; + const payload: IPayloadData = { + urlString: "https://test.com", + data: originalData, + headers: {} + }; + + return createPromise((resolve) => { + let callbackPayload: IPayloadData = null; + const callback = (processedPayload: IPayloadData) => { + callbackPayload = processedPayload; + resolve(); + }; + + // isSync = true should bypass compression even if zipPayload is true + this._sender.preparePayload(callback, true, payload, true); + + Assert.ok(callbackPayload, "Callback should be called with payload"); + Assert.equal(callbackPayload.data, originalData, "Data should remain unchanged when isSync is true"); + Assert.ok(!callbackPayload.headers["Content-Encoding"], "Content-Encoding header should not be set"); + }); + } + }); + + this.testCase({ + name: "preparePayload: empty payload data should bypass compression", + test: () => { + const payload: IPayloadData = { + urlString: "https://test.com", + data: null, + headers: {} + }; + + return createPromise((resolve) => { + let callbackPayload: IPayloadData = null; + const callback = (processedPayload: IPayloadData) => { + callbackPayload = processedPayload; + resolve(); + }; + + // null payload.data should bypass compression + this._sender.preparePayload(callback, true, payload, false); + + Assert.ok(callbackPayload, "Callback should be called with payload"); + Assert.equal(callbackPayload.data, null, "null data should remain null"); + Assert.ok(!callbackPayload.headers["Content-Encoding"], "Content-Encoding header should not be set"); + }); + } + }); + + this.testCase({ + name: "preparePayload: compression enabled with string data", + timeout: 5000, + useFakeTimers: false, + test: () => { + // Skip test if CompressionStream is not supported in this environment + if (!this._isCompressionSupported) { + Assert.ok(true, "CompressionStream is not supported in this environment, skipping test"); + return; + } + + const originalData = "This is test data for compression with string data test"; + const payload: IPayloadData = { + urlString: "https://test.com", + data: originalData, + headers: {} + }; + + return createPromise((resolve) => { + + let callbackPayload: IPayloadData = null; + const callback = (processedPayload: IPayloadData) => { + callbackPayload = processedPayload; + + Assert.ok(callbackPayload, "Callback should be called with payload"); + Assert.ok(callbackPayload.data instanceof Uint8Array, "Data should be compressed into a Uint8Array"); + Assert.equal(callbackPayload.headers["Content-Encoding"], "gzip", "Content-Encoding header should be set to gzip"); + Assert.ok((callbackPayload as any)._chunkCount >= 1, "There should be at least 1 chunk in the compressed data - [" + (callbackPayload as any)._chunkCount + "] chunks processed"); + + // Verify the compressed data can be decompressed back to original + this._decompressPayload(callbackPayload.data as Uint8Array).then(decompressedData => { + const decoder = new TextDecoder(); + const decompressedString = decoder.decode(decompressedData); + Assert.equal(decompressedString, originalData, "Decompressed data should match original"); + resolve(); + }).catch(err => { + Assert.ok(false, "Failed to decompress data: " + err); + resolve(); + }); + }; + + // Enable compression + this._sender.preparePayload(callback, true, payload, false); + }); + } + }); + + this.testCase({ + name: "preparePayload: compression with large payload requiring multiple chunks", + timeout: 10000, // Longer timeout for large payload + useFakeTimers: false, + test: () => { + // Skip test if CompressionStream is not supported in this environment + if (!this._isCompressionSupported) { + Assert.ok(true, "CompressionStream is not supported in this environment, skipping test"); + return; + } + + // Create a large payload that will likely require multiple chunks + let largePayload = "This is a large payload for compression test.\n"; + while (largePayload.length < 2000000) { + largePayload += (mathRandom().toString(36).substring(2)).repeat(2); + } + const payload: IPayloadData = { + urlString: "https://test.com", + data: largePayload, + headers: {} + }; + + return createPromise((resolve) => { + let callbackPayload: IPayloadData = null; + const callback = (processedPayload: IPayloadData) => { + callbackPayload = processedPayload; + + Assert.ok(callbackPayload, "Callback should be called with payload"); + Assert.ok(callbackPayload.data instanceof Uint8Array, "Data should be compressed into a Uint8Array"); + Assert.equal(callbackPayload.headers["Content-Encoding"], "gzip", "Content-Encoding header should be set to gzip"); + + // Verify the compressed data is smaller than the original (compression should work) + Assert.ok((callbackPayload.data as Uint8Array).length < largePayload.length, + "Compressed data should be smaller than original"); + + Assert.ok((callbackPayload as any)._chunkCount > 1, "There should be multiple chunks in the compressed data - [" + (callbackPayload as any)._chunkCount + "] chunks processed"); + + // Verify the compressed data can be decompressed back to original + this._decompressPayload(callbackPayload.data as Uint8Array).then(decompressedData => { + const decoder = new TextDecoder(); + const decompressedString = decoder.decode(decompressedData); + Assert.equal(decompressedString, largePayload, "Decompressed data should match original"); + resolve(); + }).catch(err => { + Assert.ok(false, "Failed to decompress data: " + err); + resolve(); + }); + }; + + // Enable compression + this._sender.preparePayload(callback, true, payload, false); + }); + } + }); + } + + /** + * Helper method to decompress payload data using DecompressionStream + */ + private _decompressPayload(compressedData: Uint8Array): Promise { + return new Promise((resolve, reject) => { + try { + const dsStream: any = getInst("DecompressionStream"); + if (!isFunction(dsStream)) { + reject(new Error("DecompressionStream is not supported")); + return; + } + + // Create a readable stream from compressed data + const compressedStream = new ReadableStream({ + start(controller) { + controller.enqueue(compressedData); + controller.close(); + } + }); + + // Decompress the data + const decompressedStream = compressedStream.pipeThrough(new dsStream("gzip")); + const reader = (decompressedStream.getReader() as ReadableStreamDefaultReader); + const chunks: Uint8Array[] = []; + let totalLength = 0; + + // Process each chunk from the decompressed stream reader + doAwaitResponse(reader.read(), function processChunk(response: any): undefined | Promise { + if (!response.rejected) { + // Process the chunk and continue reading + const result = response.value; + if (!result.done) { + // Add current chunk and continue reading + chunks.push(result.value); + totalLength += result.value.length; + return doAwaitResponse(reader.read(), processChunk) as any; + } + + // We are complete so combine all chunks + const combined = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + combined.set(chunk, offset); + offset += chunk.length; + } + + // Return the decompressed data + resolve(combined); + } else { + reject(response.reason); + } + }); + + } catch (error) { + reject(error); + } + }); + } +} \ No newline at end of file diff --git a/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts b/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts index bb9f2ce9e..613a53421 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts @@ -11,6 +11,7 @@ import { UpdateConfigTests } from "./UpdateConfig.Tests"; import { EventsDiscardedReasonTests } from "./EventsDiscardedReason.Tests"; import { W3cTraceParentTests } from "./W3cTraceParentTests"; import { DynamicConfigTests } from "./DynamicConfig.Tests"; +import { SendPostManagerTests } from './SendPostManager.Tests'; // import { StatsBeatTests } from './StatsBeat.Tests'; export function runTests() { @@ -29,4 +30,5 @@ export function runTests() { new W3cTraceParentTests().registerTests(); // new StatsBeatTests(false).registerTests(); // new StatsBeatTests(true).registerTests(); + new SendPostManagerTests().registerTests(); } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts b/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts index 173dbc71f..1540eeb3c 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts @@ -162,7 +162,7 @@ export class SenderPostManager { const chunks: Uint8Array[] = []; let totalLength = 0; let callbackCalled = false; - + // Process each chunk from the compressed stream reader doAwaitResponse(reader.read(), function processChunk(response: AwaitResponse>): undefined | IPromise> { if (!callbackCalled && !response.rejected) { @@ -186,6 +186,7 @@ export class SenderPostManager { // Update payload with compressed data payload.data = combined; payload.headers["Content-Encoding"] = "gzip"; + (payload as any)._chunkCount = chunks.length; } if (!callbackCalled) {