diff --git a/clients/ts/signalr/tests/ServerSentEventsTransport.test.ts b/clients/ts/signalr/tests/ServerSentEventsTransport.test.ts index 31c9e2a5dd..558f76a600 100644 --- a/clients/ts/signalr/tests/ServerSentEventsTransport.test.ts +++ b/clients/ts/signalr/tests/ServerSentEventsTransport.test.ts @@ -33,12 +33,12 @@ describe("ServerSentEventsTransport", () => { await TestEventSource.eventSource.openSet; - expect(connectComplete).toEqual(false); + expect(connectComplete).toBe(false); TestEventSource.eventSource.onopen(new TestMessageEvent()); await connectPromise; - expect(connectComplete).toEqual(true); + expect(connectComplete).toBe(true); }); }); @@ -49,7 +49,7 @@ describe("ServerSentEventsTransport", () => { await VerifyLogger.run(async (logger) => { await createAndStartSSE(logger, input, () => "secretToken"); - expect(TestEventSource.eventSource.url).toEqual(expected); + expect(TestEventSource.eventSource.url).toBe(expected); }); }); }); @@ -66,8 +66,8 @@ describe("ServerSentEventsTransport", () => { await sse.send(""); - expect(request!.headers!.Authorization).toEqual("Bearer secretToken"); - expect(request!.url).toEqual("http://example.com"); + expect(request!.headers!.Authorization).toBe("Bearer secretToken"); + expect(request!.url).toBe("http://example.com"); }); }); @@ -83,7 +83,7 @@ describe("ServerSentEventsTransport", () => { await sse.send("send data"); - expect(request!.content).toEqual("send data"); + expect(request!.content).toBe("send data"); }); }); @@ -100,8 +100,8 @@ describe("ServerSentEventsTransport", () => { message.data = "receive data"; TestEventSource.eventSource.onmessage(message); - expect(typeof received!).toEqual("string"); - expect(received!).toEqual("receive data"); + expect(typeof received!).toBe("string"); + expect(received!).toBe("receive data"); }); }); @@ -116,8 +116,8 @@ describe("ServerSentEventsTransport", () => { await sse.stop(); - expect(closeCalled).toEqual(true); - expect(TestEventSource.eventSource.closed).toEqual(true); + expect(closeCalled).toBe(true); + expect(TestEventSource.eventSource.closed).toBe(true); }); }); @@ -136,9 +136,9 @@ describe("ServerSentEventsTransport", () => { errorEvent.data = "error"; TestEventSource.eventSource.onerror(errorEvent); - expect(closeCalled).toEqual(true); - expect(TestEventSource.eventSource.closed).toEqual(true); - expect(error).toMatchObject({ message: "error" }); + expect(closeCalled).toBe(true); + expect(TestEventSource.eventSource.closed).toBe(true); + expect(error).toEqual(new Error("error")); }); }); @@ -148,7 +148,7 @@ describe("ServerSentEventsTransport", () => { await expect(sse.send("")) .rejects - .toMatchObject({ message: "Cannot send until the transport is connected" }); + .toEqual(new Error("Cannot send until the transport is connected")); }); }); @@ -171,9 +171,9 @@ describe("ServerSentEventsTransport", () => { errorEvent.data = "some data"; TestEventSource.eventSource.onmessage(errorEvent); - expect(closeCalled).toEqual(true); - expect(TestEventSource.eventSource.closed).toEqual(true); - expect(error).toMatchObject({ message: "error parsing" }); + expect(closeCalled).toBe(true); + expect(TestEventSource.eventSource.closed).toBe(true); + expect(error).toEqual(new Error("error parsing")); }); }); }); diff --git a/clients/ts/signalr/tests/TestWebSocket.ts b/clients/ts/signalr/tests/TestWebSocket.ts new file mode 100644 index 0000000000..b643958b4e --- /dev/null +++ b/clients/ts/signalr/tests/TestWebSocket.ts @@ -0,0 +1,192 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +import { PromiseSource } from "./Utils"; + +export class TestWebSocket { + public binaryType: "blob" | "arraybuffer" = "blob"; + public bufferedAmount: number = 0; + public extensions: string = ""; + public onclose!: ((this: WebSocket, ev: CloseEvent) => any); + public onerror!: ((this: WebSocket, ev: Event) => any); + public onmessage!: ((this: WebSocket, ev: MessageEvent) => any); + public protocol: string; + public readyState: number = 1; + public url: string; + + public static webSocket: TestWebSocket; + public receivedData: Array<(string | ArrayBuffer | Blob | ArrayBufferView)>; + + // tslint:disable-next-line:variable-name + private _onopen?: (this: WebSocket, evt: Event) => any; + public openSet: PromiseSource = new PromiseSource(); + public set onopen(value: (this: WebSocket, evt: Event) => any) { + this._onopen = value; + this.openSet.resolve(); + } + + public get onopen(): (this: WebSocket, evt: Event) => any { + return this._onopen!; + } + + public close(code?: number | undefined, reason?: string | undefined): void { + const closeEvent = new TestCloseEvent(); + closeEvent.code = code || 1000; + closeEvent.reason = reason!; + closeEvent.wasClean = closeEvent.code === 1000; + this.onclose(closeEvent); + } + + public send(data: string | ArrayBuffer | Blob | ArrayBufferView): void { + this.receivedData.push(data); + } + + public addEventListener(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined): void; + public addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void; + public addEventListener(type: any, listener: any, options?: any) { + throw new Error("Method not implemented."); + } + public removeEventListener(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | EventListenerOptions | undefined): void; + public removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions | undefined): void; + public removeEventListener(type: any, listener: any, options?: any) { + throw new Error("Method not implemented."); + } + public dispatchEvent(evt: Event): boolean { + throw new Error("Method not implemented."); + } + + constructor(url: string, protocols?: string | string[]) { + this.url = url; + this.protocol = protocols ? (typeof protocols === "string" ? protocols : protocols[0]) : ""; + TestWebSocket.webSocket = this; + this.receivedData = []; + } + + public readonly CLOSED: number = 1; + public static readonly CLOSED: number = 1; + public readonly CLOSING: number = 2; + public static readonly CLOSING: number = 2; + public readonly CONNECTING: number = 3; + public static readonly CONNECTING: number = 3; + public readonly OPEN: number = 4; + public static readonly OPEN: number = 4; +} + +export class TestEvent { + public bubbles: boolean = false; + public cancelBubble: boolean = false; + public cancelable: boolean = false; + public currentTarget!: EventTarget; + public defaultPrevented: boolean = false; + public eventPhase: number = 0; + public isTrusted: boolean = false; + public returnValue: boolean = false; + public scoped: boolean = false; + public srcElement!: Element | null; + public target!: EventTarget; + public timeStamp: number = 0; + public type: string = ""; + public deepPath(): EventTarget[] { + throw new Error("Method not implemented."); + } + public initEvent(type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error("Method not implemented."); + } + public preventDefault(): void { + throw new Error("Method not implemented."); + } + public stopImmediatePropagation(): void { + throw new Error("Method not implemented."); + } + public stopPropagation(): void { + throw new Error("Method not implemented."); + } + public AT_TARGET: number = 0; + public BUBBLING_PHASE: number = 0; + public CAPTURING_PHASE: number = 0; + public NONE: number = 0; +} + +export class TestErrorEvent { + public colno: number = 0; + public error: any; + public filename: string = ""; + public lineno: number = 0; + public message: string = ""; + public initErrorEvent(typeArg: string, canBubbleArg: boolean, cancelableArg: boolean, messageArg: string, filenameArg: string, linenoArg: number): void { + throw new Error("Method not implemented."); + } + public bubbles: boolean = false; + public cancelBubble: boolean = false; + public cancelable: boolean = false; + public currentTarget!: EventTarget | null; + public defaultPrevented: boolean = false; + public eventPhase: number = 0; + public isTrusted: boolean = false; + public returnValue: boolean = false; + public scoped: boolean = false; + public srcElement!: Element | null; + public target!: EventTarget | null; + public timeStamp: number = 0; + public type: string = ""; + public deepPath(): EventTarget[] { + throw new Error("Method not implemented."); + } + public initEvent(type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error("Method not implemented."); + } + public preventDefault(): void { + throw new Error("Method not implemented."); + } + public stopImmediatePropagation(): void { + throw new Error("Method not implemented."); + } + public stopPropagation(): void { + throw new Error("Method not implemented."); + } + public AT_TARGET: number = 0; + public BUBBLING_PHASE: number = 0; + public CAPTURING_PHASE: number = 0; + public NONE: number = 0; +} + +export class TestCloseEvent { + public code: number = 0; + public reason: string = ""; + public wasClean: boolean = false; + public initCloseEvent(typeArg: string, canBubbleArg: boolean, cancelableArg: boolean, wasCleanArg: boolean, codeArg: number, reasonArg: string): void { + throw new Error("Method not implemented."); + } + public bubbles: boolean = false; + public cancelBubble: boolean = false; + public cancelable: boolean = false; + public currentTarget!: EventTarget; + public defaultPrevented: boolean = false; + public eventPhase: number = 0; + public isTrusted: boolean = false; + public returnValue: boolean = false; + public scoped: boolean = false; + public srcElement!: Element | null; + public target!: EventTarget; + public timeStamp: number = 0; + public type: string = ""; + public deepPath(): EventTarget[] { + throw new Error("Method not implemented."); + } + public initEvent(type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error("Method not implemented."); + } + public preventDefault(): void { + throw new Error("Method not implemented."); + } + public stopImmediatePropagation(): void { + throw new Error("Method not implemented."); + } + public stopPropagation(): void { + throw new Error("Method not implemented."); + } + public AT_TARGET: number = 0; + public BUBBLING_PHASE: number = 0; + public CAPTURING_PHASE: number = 0; + public NONE: number = 0; +} diff --git a/clients/ts/signalr/tests/WebSocketTransport.test.ts b/clients/ts/signalr/tests/WebSocketTransport.test.ts new file mode 100644 index 0000000000..98996364fe --- /dev/null +++ b/clients/ts/signalr/tests/WebSocketTransport.test.ts @@ -0,0 +1,213 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +import { ILogger } from "../src/ILogger"; +import { TransferFormat } from "../src/ITransport"; +import { WebSocketTransport } from "../src/WebSocketTransport"; +import { VerifyLogger } from "./Common"; +import { TestMessageEvent } from "./TestEventSource"; +import { TestCloseEvent, TestEvent, TestWebSocket } from "./TestWebSocket"; + +describe("WebSocketTransport", () => { + it("sets websocket binarytype to arraybuffer on Binary transferformat", async () => { + await VerifyLogger.run(async (logger) => { + await createAndStartWebSocket(logger, "http://example.com", undefined, TransferFormat.Binary); + + expect(TestWebSocket.webSocket.binaryType).toBe("arraybuffer"); + }); + }); + + it("connect waits for WebSocket to be connected", async () => { + await VerifyLogger.run(async (logger) => { + const webSocket = new WebSocketTransport(undefined, logger, true, TestWebSocket); + + let connectComplete: boolean = false; + const connectPromise = (async () => { + await webSocket.connect("http://example.com", TransferFormat.Text); + connectComplete = true; + })(); + + await TestWebSocket.webSocket.openSet; + + expect(connectComplete).toBe(false); + + TestWebSocket.webSocket.onopen(new TestEvent()); + + await connectPromise; + expect(connectComplete).toBe(true); + }); + }); + + it("connect fails if there is error during connect", async () => { + await VerifyLogger.run(async (logger) => { + (global as any).ErrorEvent = TestEvent; + const webSocket = new WebSocketTransport(undefined, logger, true, TestWebSocket); + + let connectComplete: boolean = false; + const connectPromise = (async () => { + await webSocket.connect("http://example.com", TransferFormat.Text); + connectComplete = true; + })(); + + await TestWebSocket.webSocket.openSet; + + expect(connectComplete).toBe(false); + + TestWebSocket.webSocket.onerror(new TestEvent()); + + await expect(connectPromise) + .rejects; + expect(connectComplete).toBe(false); + }); + }); + + [["http://example.com", "ws://example.com?access_token=secretToken"], + ["http://example.com?value=null", "ws://example.com?value=null&access_token=secretToken"], + ["https://example.com?value=null", "wss://example.com?value=null&access_token=secretToken"]] + .forEach(([input, expected]) => { + it(`generates correct WebSocket URL for ${input} with access_token`, async () => { + await VerifyLogger.run(async (logger) => { + await createAndStartWebSocket(logger, input, () => "secretToken"); + + expect(TestWebSocket.webSocket.url).toBe(expected); + }); + }); + }); + + [["http://example.com", "ws://example.com"], + ["http://example.com?value=null", "ws://example.com?value=null"], + ["https://example.com?value=null", "wss://example.com?value=null"]] + .forEach(([input, expected]) => { + it(`generates correct WebSocket URL for ${input}`, async () => { + await VerifyLogger.run(async (logger) => { + await createAndStartWebSocket(logger, input, undefined); + + expect(TestWebSocket.webSocket.url).toBe(expected); + }); + }); + }); + + it("can receive data", async () => { + await VerifyLogger.run(async (logger) => { + const webSocket = await createAndStartWebSocket(logger); + + let received: string | ArrayBuffer; + webSocket.onreceive = (data) => { + received = data; + }; + + const message = new TestMessageEvent(); + message.data = "receive data"; + TestWebSocket.webSocket.onmessage(message); + + expect(typeof received!).toBe("string"); + expect(received!).toBe("receive data"); + }); + }); + + it("is closed from WebSocket onclose with error", async () => { + await VerifyLogger.run(async (logger) => { + (global as any).ErrorEvent = TestEvent; + const webSocket = await createAndStartWebSocket(logger); + + let closeCalled: boolean = false; + let error: Error; + webSocket.onclose = (e) => { + closeCalled = true; + error = e!; + }; + + const message = new TestCloseEvent(); + message.wasClean = false; + message.code = 1; + message.reason = "just cause"; + TestWebSocket.webSocket.onclose(message); + + expect(closeCalled).toBe(true); + expect(error!).toEqual(new Error("Websocket closed with status code: 1 (just cause)")); + + await expect(webSocket.send("")) + .rejects + .toThrow("WebSocket is not in the OPEN state"); + }); + }); + + it("is closed from WebSocket onclose", async () => { + await VerifyLogger.run(async (logger) => { + (global as any).ErrorEvent = TestEvent; + const webSocket = await createAndStartWebSocket(logger); + + let closeCalled: boolean = false; + let error: Error; + webSocket.onclose = (e) => { + closeCalled = true; + error = e!; + }; + + const message = new TestCloseEvent(); + message.wasClean = true; + message.code = 1000; + message.reason = "success"; + TestWebSocket.webSocket.onclose(message); + + expect(closeCalled).toBe(true); + expect(error!).toBeUndefined(); + + await expect(webSocket.send("")) + .rejects + .toThrow("WebSocket is not in the OPEN state"); + }); + }); + + it("is closed from Transport stop", async () => { + await VerifyLogger.run(async (logger) => { + (global as any).ErrorEvent = TestEvent; + const webSocket = await createAndStartWebSocket(logger); + + let closeCalled: boolean = false; + let error: Error; + webSocket.onclose = (e) => { + closeCalled = true; + error = e!; + }; + + await webSocket.stop(); + + expect(closeCalled).toBe(true); + expect(error!).toBeUndefined(); + + await expect(webSocket.send("")) + .rejects + .toThrow("WebSocket is not in the OPEN state"); + }); + }); + + [[TransferFormat.Text, "send data"], + [TransferFormat.Binary, new Uint8Array([0, 1, 3])]] + .forEach(([format, data]) => { + it(`can send ${TransferFormat[format as TransferFormat]} data`, async () => { + await VerifyLogger.run(async (logger) => { + const webSocket = await createAndStartWebSocket(logger, "http://example.com", undefined, format as TransferFormat); + + TestWebSocket.webSocket.readyState = TestWebSocket.OPEN; + await webSocket.send(data); + + expect(TestWebSocket.webSocket.receivedData.length).toBe(1); + expect(TestWebSocket.webSocket.receivedData[0]).toBe(data); + }); + }); + }); +}); + +async function createAndStartWebSocket(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise), format?: TransferFormat): Promise { + const webSocket = new WebSocketTransport(accessTokenFactory, logger, true, TestWebSocket); + + const connectPromise = webSocket.connect(url || "http://example.com", format || TransferFormat.Text); + + await TestWebSocket.webSocket.openSet; + TestWebSocket.webSocket.onopen(new TestEvent()); + + await connectPromise; + + return webSocket; +}