// 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 { HubConnection, LogLevel, TransportType } from "@aspnet/signalr"; import { eachTransport, eachTransportAndProtocol } from "./Common"; const TESTHUBENDPOINT_URL = "/testhub"; describe("hubConnection", () => { eachTransportAndProtocol((transportType, protocol) => { describe("using " + protocol.name + " over " + TransportType[transportType] + " transport", () => { it("can invoke server method and receive result", (done) => { const message = "你好,世界!"; const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.onclose((error) => { expect(error).toBe(undefined); done(); }); hubConnection.start().then(() => { hubConnection.invoke("Echo", message).then((result) => { expect(result).toBe(message); }).catch((e) => { fail(e); }).then(() => { hubConnection.stop(); }); }).catch((e) => { fail(e); done(); }); }); it("can invoke server method non-blocking and not receive result", (done) => { const message = "你好,世界!"; const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.onclose((error) => { expect(error).toBe(undefined); done(); }); hubConnection.start().then(() => { hubConnection.send("Echo", message).catch((e) => { fail(e); }).then(() => { hubConnection.stop(); }); }).catch((e) => { fail(e); done(); }); }); it("can invoke server method structural object and receive structural result", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.on("CustomObject", (customObject) => { expect(customObject.Name).toBe("test"); expect(customObject.Value).toBe(42); hubConnection.stop(); }); hubConnection.onclose((error) => { expect(error).toBe(undefined); done(); }); hubConnection.start().then(() => { hubConnection.send("SendCustomObject", { Name: "test", Value: 42 }); }).catch((e) => { fail(e); done(); }); }); it("can stream server method and receive result", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.onclose((error) => { expect(error).toBe(undefined); done(); }); const received = []; hubConnection.start().then(() => { hubConnection.stream("Stream").subscribe({ complete: function complete() { expect(received).toEqual(["a", "b", "c"]); hubConnection.stop(); }, error: function error(err) { fail(err); hubConnection.stop(); }, next: function next(item) { received.push(item); }, }); }).catch((e) => { fail(e); done(); }); }); it("rethrows an exception from the server when invoking", (done) => { const errorMessage = "An error occurred."; const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.start().then(() => { hubConnection.invoke("ThrowException", errorMessage).then(() => { // exception expected but none thrown fail(); }).catch((e) => { expect(e.message).toBe(errorMessage); }).then(() => { return hubConnection.stop(); }).then(() => { done(); }); }).catch((e) => { fail(e); done(); }); }); it("throws an exception when invoking streaming method with invoke", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.start().then(() => { hubConnection.invoke("EmptyStream").then(() => { // exception expected but none thrown fail(); }).catch((e) => { expect(e.message).toBe("The client attempted to invoke the streaming 'EmptyStream' method in a non-streaming fashion."); }).then(() => { return hubConnection.stop(); }).then(() => { done(); }); }).catch((e) => { fail(e); done(); }); }); it("throws an exception when receiving a streaming result for method called with invoke", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.start().then(() => { hubConnection.invoke("Stream").then(() => { // exception expected but none thrown fail(); }).catch((e) => { expect(e.message).toBe("The client attempted to invoke the streaming 'Stream' method in a non-streaming fashion."); }).then(() => { return hubConnection.stop(); }).then(() => { done(); }); }).catch((e) => { fail(e); done(); }); }); it("rethrows an exception from the server when streaming", (done) => { const errorMessage = "An error occurred."; const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.start().then(() => { hubConnection.stream("StreamThrowException", errorMessage).subscribe({ complete: function complete() { hubConnection.stop(); fail(); }, error: function error(err) { expect(err.message).toEqual("An error occurred."); hubConnection.stop(); done(); }, next: function next(item) { hubConnection.stop(); fail(); }, }); }).catch((e) => { fail(e); done(); }); }); it("throws an exception when invoking hub method with stream", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.start().then(() => { hubConnection.stream("Echo", "42").subscribe({ complete: function complete() { hubConnection.stop(); fail(); }, error: function error(err) { expect(err.message).toEqual("The client attempted to invoke the non-streaming 'Echo' method in a streaming fashion."); hubConnection.stop(); done(); }, next: function next(item) { hubConnection.stop(); fail(); }, }); }).catch((e) => { fail(e); done(); }); }); it("can receive server calls", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); const message = "你好 SignalR!"; // client side method names are case insensitive let methodName = "message"; const idx = Math.floor(Math.random() * (methodName.length - 1)); methodName = methodName.substr(0, idx) + methodName[idx].toUpperCase() + methodName.substr(idx + 1); hubConnection.on(methodName, (msg) => { expect(msg).toBe(message); done(); }); hubConnection.start() .then(() => { return hubConnection.invoke("InvokeWithString", message); }) .then(() => { return hubConnection.stop(); }) .catch((e) => { fail(e); done(); }); }); it("can receive server calls without rebinding handler when restarted", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); const message = "你好 SignalR!"; // client side method names are case insensitive let methodName = "message"; const idx = Math.floor(Math.random() * (methodName.length - 1)); methodName = methodName.substr(0, idx) + methodName[idx].toUpperCase() + methodName.substr(idx + 1); let closeCount = 0; let invocationCount = 0; hubConnection.onclose((e) => { expect(e).toBeUndefined(); closeCount += 1; if (closeCount === 1) { // Reconnect hubConnection.start() .then(() => { return hubConnection.invoke("InvokeWithString", message); }) .then(() => { return hubConnection.stop(); }) .catch((error) => { fail(error); done(); }); } else { expect(invocationCount).toBe(2); done(); } }); hubConnection.on(methodName, (msg) => { expect(msg).toBe(message); invocationCount += 1; }); hubConnection.start() .then(() => { return hubConnection.invoke("InvokeWithString", message); }) .then(() => { return hubConnection.stop(); }) .catch((e) => { fail(e); done(); }); }); it("closed with error if hub cannot be created", (done) => { const errorRegex = { LongPolling: "Internal Server Error", ServerSentEvents: "Error occurred", WebSockets: "1011|1005", // Message is browser specific (e.g. 'Websocket closed with status code: 1011'), Edge and IE report 1005 even though the server sent 1011 }; const hubConnection = new HubConnection("http://" + document.location.host + "/uncreatable", { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.onclose((error) => { expect(error.message).toMatch(errorRegex[TransportType[transportType]]); done(); }); hubConnection.start(); }); it("can handle different types", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); hubConnection.onclose((error) => { expect(error).toBe(undefined); done(); }); const complexObject = { ByteArray: protocol.name === "json" ? "aGVsbG8=" : new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]), GUID: protocol.name === "json" ? "00010203-0405-0607-0706-050403020100" : new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00]), IntArray: [0x01, 0x02, 0x03, 0xff], String: "Hello, World!", }; hubConnection.start() .then(() => { return hubConnection.invoke("EchoComplexObject", complexObject); }) .then((value) => { if (protocol.name === "messagepack") { // msgpack creates a Buffer for byte arrays and jasmine fails to compare a Buffer // and a Uint8Array even though Buffer instances are also Uint8Array instances value.ByteArray = new Uint8Array(value.ByteArray); // GUIDs are serialized as raw type which is a string containing bytes which need to // be extracted. Note that with msgpack5 the original bytes will be encoded with utf8 // and needs to be decoded. To not go into utf8 encoding intricacies the test uses values // less than 0x80. const guidBytes = []; for (let i = 0; i < value.GUID.length; i++) { guidBytes.push(value.GUID.charCodeAt(i)); } value.GUID = new Uint8Array(guidBytes); } expect(value).toEqual(complexObject); }) .then(() => { hubConnection.stop(); }) .catch((e) => { fail(e); done(); }); }); it("can be restarted", (done) => { const message = "你好,世界!"; const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, protocol, transport: transportType, }); let closeCount = 0; hubConnection.onclose((error) => { expect(error).toBe(undefined); // Start and invoke again if (closeCount === 0) { closeCount += 1; hubConnection.start().then(() => { hubConnection.invoke("Echo", message).then((result) => { expect(result).toBe(message); }).catch((e) => { fail(e); }).then(() => { hubConnection.stop(); }); }).catch((e) => { fail(e); done(); }); } else { done(); } }); hubConnection.start().then(() => { hubConnection.invoke("Echo", message).then((result) => { expect(result).toBe(message); }).catch((e) => { fail(e); }).then(() => { hubConnection.stop(); }); }).catch((e) => { fail(e); done(); }); }); }); }); eachTransport((transportType) => { describe("over " + TransportType[transportType] + " transport", () => { it("can connect to hub with authorization", async (done) => { const message = "你好,世界!"; let hubConnection; getJwtToken("http://" + document.location.host + "/generateJwtToken") .then((jwtToken) => { hubConnection = new HubConnection("/authorizedhub", { accessTokenFactory: () => jwtToken, logger: LogLevel.Trace, transport: transportType, }); hubConnection.onclose((error) => { expect(error).toBe(undefined); done(); }); return hubConnection.start(); }) .then(() => { return hubConnection.invoke("Echo", message); }) .then((response) => { expect(response).toEqual(message); done(); }) .catch((err) => { fail(err); done(); }); }); if (transportType !== TransportType.LongPolling) { it("terminates if no messages received within timeout interval", (done) => { const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, { logger: LogLevel.Trace, timeoutInMilliseconds: 100, transport: transportType, }); const timeout = setTimeout(200, () => { fail("Server timeout did not fire within expected interval"); }); hubConnection.start().then(() => { hubConnection.onclose((error) => { clearTimeout(timeout); expect(error).toEqual(new Error("Server timeout elapsed without receiving a message from the server.")); done(); }); }); }); } }); }); function getJwtToken(url): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.send(); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.response || xhr.responseText); } else { reject(new Error(xhr.statusText)); } }; xhr.onerror = () => { reject(new Error(xhr.statusText)); }; }); } });