From cb8264321dd6235d242db7e61b80fad33a8c6250 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Tue, 12 Jun 2018 12:49:15 -0700 Subject: [PATCH] Add VerifyLogger to JS tests (#2472) --- clients/ts/signalr/tests/Common.ts | 37 + .../ts/signalr/tests/HttpConnection.test.ts | 1156 +++++++------ .../ts/signalr/tests/HubConnection.test.ts | 1487 +++++++++-------- .../tests/HubConnectionBuilder.test.ts | 95 +- .../ts/signalr/tests/JsonHubProtocol.test.ts | 182 +- .../tests/LongPollingTransport.test.ts | 140 +- 6 files changed, 1672 insertions(+), 1425 deletions(-) diff --git a/clients/ts/signalr/tests/Common.ts b/clients/ts/signalr/tests/Common.ts index ae3479ba8b..7a07a35d3d 100644 --- a/clients/ts/signalr/tests/Common.ts +++ b/clients/ts/signalr/tests/Common.ts @@ -1,6 +1,8 @@ // 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 { EOL } from "os"; +import { ILogger, LogLevel } from "../src/ILogger"; import { HttpTransportType } from "../src/ITransport"; export function eachTransport(action: (transport: HttpTransportType) => void) { @@ -21,3 +23,38 @@ export function eachEndpointUrl(action: (givenUrl: string, expectedUrl: string) urls.forEach((t) => action(t[0], t[1])); } + +type ErrorMatchFunction = (error: string) => boolean; + +export class VerifyLogger implements ILogger { + public unexpectedErrors: string[]; + private expectedErrors: ErrorMatchFunction[]; + + public constructor(...expectedErrors: Array) { + this.unexpectedErrors = []; + this.expectedErrors = []; + expectedErrors.forEach((element) => { + if (element instanceof RegExp) { + this.expectedErrors.push((e) => element.test(e)); + } else if (typeof element === "string") { + this.expectedErrors.push((e) => element === e); + } else { + this.expectedErrors.push(element); + } + }, this); + } + + public static async run(fn: (logger: VerifyLogger) => Promise, ...expectedErrors: Array): Promise { + const logger = new VerifyLogger(...expectedErrors); + await fn(logger); + expect(logger.unexpectedErrors.join(EOL)).toBe(""); + } + + public log(logLevel: LogLevel, message: string): void { + if (logLevel >= LogLevel.Error) { + if (!this.expectedErrors.some((fn) => fn(message))) { + this.unexpectedErrors.push(message); + } + } + } +} diff --git a/clients/ts/signalr/tests/HttpConnection.test.ts b/clients/ts/signalr/tests/HttpConnection.test.ts index dcce611d87..a4c45d5150 100644 --- a/clients/ts/signalr/tests/HttpConnection.test.ts +++ b/clients/ts/signalr/tests/HttpConnection.test.ts @@ -8,9 +8,9 @@ import { HttpTransportType, ITransport, TransferFormat } from "../src/ITransport import { HttpError } from "../src/Errors"; import { NullLogger } from "../src/Loggers"; -import { EventSourceConstructor } from "../src/Polyfills"; +import { EventSourceConstructor, WebSocketConstructor } from "../src/Polyfills"; -import { eachEndpointUrl, eachTransport } from "./Common"; +import { eachEndpointUrl, eachTransport, VerifyLogger } from "./Common"; import { TestHttpClient } from "./TestHttpClient"; import { PromiseSource } from "./Utils"; @@ -42,208 +42,242 @@ describe("HttpConnection", () => { }); it("starting connection fails if getting id fails", async () => { - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => Promise.reject("error")) - .on("GET", () => ""), - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("error"); - }); - - it("cannot start a running connection", async () => { - const negotiating = new PromiseSource(); - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => { - negotiating.resolve(); - return defaultNegotiateResponse; - }), - transport: { - connect() { - return Promise.resolve(); - }, - send() { - return Promise.resolve(); - }, - stop() { - return Promise.resolve(); - }, - onclose: null, - onreceive: null, - }, - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - try { - await connection.start(TransferFormat.Text); - - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Cannot start a connection that is not in the 'Disconnected' state."); - } finally { - await connection.stop(); - } - }); - - it("can start a stopped connection", async () => { - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => { - return Promise.reject("reached negotiate"); - }) - .on("GET", () => ""), - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("reached negotiate"); - - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("reached negotiate"); - }); - - it("can stop a starting connection", async () => { - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", async () => { - await connection.stop(); - return "{}"; - }) - .on("GET", async () => { - await connection.stop(); - return ""; - }), - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - - await connection.start(TransferFormat.Text); - }); - - it("can stop a non-started connection", async () => { - const connection = new HttpConnection("http://tempuri.org", commonOptions); - await connection.stop(); - }); - - it("start throws after all transports fail", async () => { - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => ({ connectionId: "42", availableTransports: [] })) - .on("GET", () => { throw new Error("fail"); }), - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org?q=myData", options); - - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Unable to initialize any of the available transports."); - }); - - it("preserves user's query string", async () => { - const connectUrl = new PromiseSource(); - const fakeTransport: ITransport = { - connect(url: string): Promise { - connectUrl.resolve(url); - return Promise.resolve(); - }, - send(): Promise { - return Promise.resolve(); - }, - stop(): Promise { - return Promise.resolve(); - }, - onclose: null, - onreceive: null, - }; - - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => "{ \"connectionId\": \"42\" }") - .on("GET", () => ""), - transport: fakeTransport, - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org?q=myData", options); - try { - const startPromise = connection.start(TransferFormat.Text); - - expect(await connectUrl).toBe("http://tempuri.org?q=myData&id=42"); - - await startPromise; - } finally { - await connection.stop(); - } - }); - - eachEndpointUrl((givenUrl: string, expectedUrl: string) => { - it(`negotiate request for '${givenUrl}' puts 'negotiate' at the end of the path`, async () => { - const negotiateUrl = new PromiseSource(); + await VerifyLogger.run(async (logger) => { const options: IHttpConnectionOptions = { ...commonOptions, httpClient: new TestHttpClient() - .on("POST", (r) => { - negotiateUrl.resolve(r.url); - throw new HttpError("We don't care how this turns out", 500); - }) - .on("GET", () => { - return new HttpResponse(204); - }) - .on("DELETE", () => new HttpResponse(202)), - } as IHttpConnectionOptions; - - const connection = new HttpConnection(givenUrl, options); - try { - const startPromise = connection.start(TransferFormat.Text); - - expect(await negotiateUrl).toBe(expectedUrl); - - await expect(startPromise).rejects; - } finally { - await connection.stop(); - } - }); - }); - - eachTransport((requestedTransport: HttpTransportType) => { - it(`cannot be started if requested ${HttpTransportType[requestedTransport]} transport not available on server`, async () => { - // Clone the default response - const negotiateResponse = { ...defaultNegotiateResponse }; - - // Remove the requested transport from the response - negotiateResponse.availableTransports = negotiateResponse.availableTransports! - .filter((f) => f.transport !== HttpTransportType[requestedTransport]); - - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => negotiateResponse) - .on("GET", () => new HttpResponse(204)), - transport: requestedTransport, + .on("POST", () => Promise.reject("error")) + .on("GET", () => ""), + logger, } as IHttpConnectionOptions; const connection = new HttpConnection("http://tempuri.org", options); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("error"); + }, + "Failed to start the connection: error", + "Failed to complete negotiation with the server: error"); + }); + + it("cannot start a running connection", async () => { + await VerifyLogger.run(async (logger) => { + const negotiating = new PromiseSource(); + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => { + negotiating.resolve(); + return defaultNegotiateResponse; + }), + logger, + transport: { + connect() { + return Promise.resolve(); + }, + send() { + return Promise.resolve(); + }, + stop() { + return Promise.resolve(); + }, + onclose: null, + onreceive: null, + }, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org", options); + try { + await connection.start(TransferFormat.Text); + + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Cannot start a connection that is not in the 'Disconnected' state."); + } finally { + await connection.stop(); + } + }); + }); + + it("can start a stopped connection", async () => { + await VerifyLogger.run(async (logger) => { + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => { + return Promise.reject("reached negotiate"); + }) + .on("GET", () => ""), + logger, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org", options); + + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("reached negotiate"); + + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("reached negotiate"); + }, + "Failed to complete negotiation with the server: reached negotiate", + "Failed to start the connection: reached negotiate"); + }); + + it("can stop a starting connection", async () => { + await VerifyLogger.run(async (logger) => { + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", async () => { + await connection.stop(); + return "{}"; + }) + .on("GET", async () => { + await connection.stop(); + return ""; + }), + logger, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org", options); + + await connection.start(TransferFormat.Text); + }); + }); + + it("can stop a non-started connection", async () => { + await VerifyLogger.run(async (logger) => { + const connection = new HttpConnection("http://tempuri.org", { ...commonOptions, logger }); + await connection.stop(); + }); + }); + + it("start throws after all transports fail", async () => { + await VerifyLogger.run(async (logger) => { + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => ({ connectionId: "42", availableTransports: [] })) + .on("GET", () => { throw new Error("fail"); }), + logger, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org?q=myData", options); + await expect(connection.start(TransferFormat.Text)) .rejects .toThrow("Unable to initialize any of the available transports."); + }, + "Failed to start the connection: Error: Unable to initialize any of the available transports."); + }); + + it("preserves user's query string", async () => { + await VerifyLogger.run(async (logger) => { + const connectUrl = new PromiseSource(); + const fakeTransport: ITransport = { + connect(url: string): Promise { + connectUrl.resolve(url); + return Promise.resolve(); + }, + send(): Promise { + return Promise.resolve(); + }, + stop(): Promise { + return Promise.resolve(); + }, + onclose: null, + onreceive: null, + }; + + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => "{ \"connectionId\": \"42\" }") + .on("GET", () => ""), + logger, + transport: fakeTransport, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org?q=myData", options); + try { + const startPromise = connection.start(TransferFormat.Text); + + expect(await connectUrl).toBe("http://tempuri.org?q=myData&id=42"); + + await startPromise; + } finally { + await connection.stop(); + } + }); + }); + + eachEndpointUrl((givenUrl: string, expectedUrl: string) => { + it(`negotiate request for '${givenUrl}' puts 'negotiate' at the end of the path`, async () => { + await VerifyLogger.run(async (logger) => { + const negotiateUrl = new PromiseSource(); + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", (r) => { + negotiateUrl.resolve(r.url); + throw new HttpError("We don't care how this turns out", 500); + }) + .on("GET", () => { + return new HttpResponse(204); + }) + .on("DELETE", () => new HttpResponse(202)), + logger, + } as IHttpConnectionOptions; + + const connection = new HttpConnection(givenUrl, options); + try { + const startPromise = connection.start(TransferFormat.Text); + + expect(await negotiateUrl).toBe(expectedUrl); + + await expect(startPromise).rejects; + } finally { + await connection.stop(); + } + }, + "Failed to complete negotiation with the server: Error: We don't care how this turns out", + "Failed to start the connection: Error: We don't care how this turns out"); + }); + }); + + eachTransport((requestedTransport: HttpTransportType) => { + it(`cannot be started if requested ${HttpTransportType[requestedTransport]} transport not available on server`, async () => { + await VerifyLogger.run(async (logger) => { + // Clone the default response + const negotiateResponse = { ...defaultNegotiateResponse }; + + // Remove the requested transport from the response + negotiateResponse.availableTransports = negotiateResponse.availableTransports! + .filter((f) => f.transport !== HttpTransportType[requestedTransport]); + + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => negotiateResponse) + .on("GET", () => new HttpResponse(204)), + logger, + transport: requestedTransport, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org", options); + + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Unable to initialize any of the available transports."); + }, + "Failed to start the connection: Error: Unable to initialize any of the available transports."); }); - for (const [val, name] of [[null, "null"], [undefined, "undefined"], [0, "0"]]) { - it(`can be started using ${HttpTransportType[requestedTransport]} transport when transport mask is ${name}`, async () => { + it(`cannot be started if server's only transport (${HttpTransportType[requestedTransport]}) is masked out by the transport option`, async () => { + await VerifyLogger.run(async (logger) => { const negotiateResponse = { availableTransports: [ { transport: "WebSockets", transferFormats: ["Text", "Binary"] }, @@ -253,324 +287,358 @@ describe("HttpConnection", () => { connectionId: "abc123", }; + // Build the mask by inverting the requested transport + const transportMask = ~requestedTransport; + + // Remove all transports other than the requested one + negotiateResponse.availableTransports = negotiateResponse.availableTransports + .filter((r) => r.transport === HttpTransportType[requestedTransport]); + const options: IHttpConnectionOptions = { ...commonOptions, httpClient: new TestHttpClient() .on("POST", () => negotiateResponse) .on("GET", () => new HttpResponse(204)), + logger, + transport: transportMask, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org", options); + + try { + await connection.start(TransferFormat.Text); + fail("Expected connection.start to throw!"); + } catch (e) { + expect(e.message).toBe("Unable to initialize any of the available transports."); + } + }, + "Failed to start the connection: Error: Unable to initialize any of the available transports."); + }); + }); + + for (const [val, name] of [[null, "null"], [undefined, "undefined"], [0, "0"]]) { + it(`can be started when transport mask is ${name}`, async () => { + await VerifyLogger.run(async (logger) => { + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => defaultNegotiateResponse) + .on("GET", () => new HttpResponse(200)) + .on("DELETE", () => new HttpResponse(202)), + logger, transport: val, } as IHttpConnectionOptions; const connection = new HttpConnection("http://tempuri.org", options); await connection.start(TransferFormat.Text); + + await connection.stop(); }); - } - - it(`cannot be started if server's only transport (${HttpTransportType[requestedTransport]}) is masked out by the transport option`, async () => { - const negotiateResponse = { - availableTransports: [ - { transport: "WebSockets", transferFormats: ["Text", "Binary"] }, - { transport: "ServerSentEvents", transferFormats: ["Text"] }, - { transport: "LongPolling", transferFormats: ["Text", "Binary"] }, - ], - connectionId: "abc123", - }; - - // Build the mask by inverting the requested transport - const transportMask = ~requestedTransport; - - // Remove all transports other than the requested one - negotiateResponse.availableTransports = negotiateResponse.availableTransports - .filter((r) => r.transport === HttpTransportType[requestedTransport]); + }); + } + it("cannot be started if no transport available on server and no transport requested", async () => { + await VerifyLogger.run(async (logger) => { const options: IHttpConnectionOptions = { ...commonOptions, httpClient: new TestHttpClient() - .on("POST", () => negotiateResponse) - .on("GET", () => new HttpResponse(204)), - transport: transportMask, + .on("POST", () => ({ connectionId: "42", availableTransports: [] })) + .on("GET", () => ""), + logger, } as IHttpConnectionOptions; const connection = new HttpConnection("http://tempuri.org", options); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Unable to initialize any of the available transports."); + }, + "Failed to start the connection: Error: Unable to initialize any of the available transports."); + }); + it("does not send negotiate request if WebSockets transport requested explicitly and skipNegotiation is true", async () => { + await VerifyLogger.run(async (logger) => { + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient(), + logger, + skipNegotiation: true, + transport: HttpTransportType.WebSockets, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org", options); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("'WebSocket' is not supported in your environment."); + }, + "Failed to start the connection: Error: 'WebSocket' is not supported in your environment."); + }); + + it("does not start non WebSockets transport if requested explicitly and skipNegotiation is true", async () => { + await VerifyLogger.run(async (logger) => { + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient(), + logger, + skipNegotiation: true, + transport: HttpTransportType.LongPolling, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org", options); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Negotiation can only be skipped when using the WebSocket transport directly."); + }, + "Failed to start the connection: Error: Negotiation can only be skipped when using the WebSocket transport directly."); + }); + + it("redirects to url when negotiate returns it", async () => { + await VerifyLogger.run(async (logger) => { + let firstNegotiate = true; + let firstPoll = true; + const httpClient = new TestHttpClient() + .on("POST", /negotiate$/, () => { + if (firstNegotiate) { + firstNegotiate = false; + return { url: "https://another.domain.url/chat" }; + } + return { + availableTransports: [{ transport: "LongPolling", transferFormats: ["Text"] }], + connectionId: "0rge0d00-0040-0030-0r00-000q00r00e00", + }; + }) + .on("GET", () => { + if (firstPoll) { + firstPoll = false; + return ""; + } + return new HttpResponse(204, "No Content", ""); + }) + .on("DELETE", () => new HttpResponse(202)); + + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient, + logger, + transport: HttpTransportType.LongPolling, + } as IHttpConnectionOptions; + + const connection = new HttpConnection("http://tempuri.org", options); try { await connection.start(TransferFormat.Text); - fail("Expected connection.start to throw!"); - } catch (e) { - expect(e.message).toBe("Unable to initialize any of the available transports."); + + expect(httpClient.sentRequests.length).toBe(4); + expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate"); + expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate"); + expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i); + expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i); + } finally { + await connection.stop(); } }); }); - it("cannot be started if no transport available on server and no transport requested", async () => { - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => ({ connectionId: "42", availableTransports: [] })) - .on("GET", () => ""), - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Unable to initialize any of the available transports."); - }); - - it("does not send negotiate request if WebSockets transport requested explicitly and skipNegotiation is true", async () => { - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient(), - skipNegotiation: true, - transport: HttpTransportType.WebSockets, - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("'WebSocket' is not supported in your environment."); - }); - - it("does not start non WebSockets transport requested explicitly and skipNegotiation is true", async () => { - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient(), - skipNegotiation: true, - transport: HttpTransportType.LongPolling, - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Negotiation can only be skipped when using the WebSocket transport directly."); - }); - - it("redirects to url when negotiate returns it", async () => { - let firstNegotiate = true; - let firstPoll = true; - const httpClient = new TestHttpClient() - .on("POST", /negotiate$/, () => { - if (firstNegotiate) { - firstNegotiate = false; - return { url: "https://another.domain.url/chat" }; - } - return { - availableTransports: [{ transport: "LongPolling", transferFormats: ["Text"] }], - connectionId: "0rge0d00-0040-0030-0r00-000q00r00e00", - }; - }) - .on("GET", () => { - if (firstPoll) { - firstPoll = false; - return ""; - } - return new HttpResponse(204, "No Content", ""); - }) - .on("DELETE", () => new HttpResponse(202)); - - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient, - transport: HttpTransportType.LongPolling, - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - try { - await connection.start(TransferFormat.Text); - - expect(httpClient.sentRequests.length).toBe(4); - expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate"); - expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate"); - expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i); - expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i); - } finally { - await connection.stop(); - } - }); - it("fails to start if negotiate redirects more than 100 times", async () => { - const httpClient = new TestHttpClient() - .on("POST", /negotiate$/, () => ({ url: "https://another.domain.url/chat" })); + await VerifyLogger.run(async (logger) => { + const httpClient = new TestHttpClient() + .on("POST", /negotiate$/, () => ({ url: "https://another.domain.url/chat" })); - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient, - transport: HttpTransportType.LongPolling, - } as IHttpConnectionOptions; + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient, + logger, + transport: HttpTransportType.LongPolling, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Negotiate redirection limit exceeded."); + const connection = new HttpConnection("http://tempuri.org", options); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Negotiate redirection limit exceeded."); + }, + "Failed to start the connection: Error: Negotiate redirection limit exceeded."); }); it("redirects to url when negotiate returns it with access token", async () => { - let firstNegotiate = true; - let firstPoll = true; - const httpClient = new TestHttpClient() - .on("POST", /negotiate$/, (r) => { - if (firstNegotiate) { - firstNegotiate = false; + await VerifyLogger.run(async (logger) => { + let firstNegotiate = true; + let firstPoll = true; + const httpClient = new TestHttpClient() + .on("POST", /negotiate$/, (r) => { + if (firstNegotiate) { + firstNegotiate = false; - if (r.headers && r.headers.Authorization !== "Bearer firstSecret") { + if (r.headers && r.headers.Authorization !== "Bearer firstSecret") { + return new HttpResponse(401, "Unauthorized", ""); + } + + return { url: "https://another.domain.url/chat", accessToken: "secondSecret" }; + } + + if (r.headers && r.headers.Authorization !== "Bearer secondSecret") { return new HttpResponse(401, "Unauthorized", ""); } - return { url: "https://another.domain.url/chat", accessToken: "secondSecret" }; - } + return { + availableTransports: [{ transport: "LongPolling", transferFormats: ["Text"] }], + connectionId: "0rge0d00-0040-0030-0r00-000q00r00e00", + }; + }) + .on("GET", (r) => { + if (r.headers && r.headers.Authorization !== "Bearer secondSecret") { + return new HttpResponse(401, "Unauthorized", ""); + } - if (r.headers && r.headers.Authorization !== "Bearer secondSecret") { - return new HttpResponse(401, "Unauthorized", ""); - } + if (firstPoll) { + firstPoll = false; + return ""; + } + return new HttpResponse(204, "No Content", ""); + }) + .on("DELETE", () => new HttpResponse(202)); - return { - availableTransports: [{ transport: "LongPolling", transferFormats: ["Text"] }], - connectionId: "0rge0d00-0040-0030-0r00-000q00r00e00", - }; - }) - .on("GET", (r) => { - if (r.headers && r.headers.Authorization !== "Bearer secondSecret") { - return new HttpResponse(401, "Unauthorized", ""); - } + const options: IHttpConnectionOptions = { + ...commonOptions, + accessTokenFactory: () => "firstSecret", + httpClient, + logger, + transport: HttpTransportType.LongPolling, + } as IHttpConnectionOptions; - if (firstPoll) { - firstPoll = false; - return ""; - } - return new HttpResponse(204, "No Content", ""); - }) - .on("DELETE", () => new HttpResponse(202)); + const connection = new HttpConnection("http://tempuri.org", options); + try { + await connection.start(TransferFormat.Text); - const options: IHttpConnectionOptions = { - ...commonOptions, - accessTokenFactory: () => "firstSecret", - httpClient, - transport: HttpTransportType.LongPolling, - } as IHttpConnectionOptions; - - const connection = new HttpConnection("http://tempuri.org", options); - try { - await connection.start(TransferFormat.Text); - - expect(httpClient.sentRequests.length).toBe(4); - expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate"); - expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate"); - expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i); - expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i); - } finally { - await connection.stop(); - } + expect(httpClient.sentRequests.length).toBe(4); + expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate"); + expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate"); + expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i); + expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i); + } finally { + await connection.stop(); + } + }); }); it("authorization header removed when token factory returns null and using LongPolling", async () => { - const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] }; + await VerifyLogger.run(async (logger) => { + const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] }; - let httpClientGetCount = 0; - let accessTokenFactoryCount = 0; - const options: IHttpConnectionOptions = { - ...commonOptions, - accessTokenFactory: () => { - accessTokenFactoryCount++; - if (accessTokenFactoryCount === 1) { - return "A token value"; - } else { - // Return a null value after the first call to test the header being removed - return null; - } - }, - httpClient: new TestHttpClient() - .on("POST", () => ({ connectionId: "42", availableTransports: [availableTransport] })) - .on("GET", (r) => { - httpClientGetCount++; - // tslint:disable-next-line:no-string-literal - const authorizationValue = r.headers!["Authorization"]; - if (httpClientGetCount === 1) { - if (authorizationValue) { - fail("First long poll request should have a authorization header."); - } - // First long polling request must succeed so start completes - return ""; + let httpClientGetCount = 0; + let accessTokenFactoryCount = 0; + const options: IHttpConnectionOptions = { + ...commonOptions, + accessTokenFactory: () => { + accessTokenFactoryCount++; + if (accessTokenFactoryCount === 1) { + return "A token value"; } else { - // Check second long polling request has its header removed - if (authorizationValue) { - fail("Second long poll request should have no authorization header."); - } - throw new Error("fail"); + // Return a null value after the first call to test the header being removed + return null; } - }) - .on("DELETE", () => new HttpResponse(202)), - } as IHttpConnectionOptions; + }, + httpClient: new TestHttpClient() + .on("POST", () => ({ connectionId: "42", availableTransports: [availableTransport] })) + .on("GET", (r) => { + httpClientGetCount++; + // tslint:disable-next-line:no-string-literal + const authorizationValue = r.headers!["Authorization"]; + if (httpClientGetCount === 1) { + if (authorizationValue) { + fail("First long poll request should have a authorization header."); + } + // First long polling request must succeed so start completes + return ""; + } else { + // Check second long polling request has its header removed + if (authorizationValue) { + fail("Second long poll request should have no authorization header."); + } + } + }) + .on("DELETE", () => new HttpResponse(202)), + logger, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); - try { - await connection.start(TransferFormat.Text); + const connection = new HttpConnection("http://tempuri.org", options); + try { + await connection.start(TransferFormat.Text); - expect(httpClientGetCount).toBeGreaterThanOrEqual(2); - expect(accessTokenFactoryCount).toBeGreaterThanOrEqual(2); - } finally { - await connection.stop(); - } + expect(httpClientGetCount).toBeGreaterThanOrEqual(2); + expect(accessTokenFactoryCount).toBeGreaterThanOrEqual(2); + } finally { + await connection.stop(); + } + }); }); it("sets inherentKeepAlive feature when using LongPolling", async () => { - const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] }; + await VerifyLogger.run(async (logger) => { + const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] }; - let httpClientGetCount = 0; - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => ({ connectionId: "42", availableTransports: [availableTransport] })) - .on("GET", () => { - httpClientGetCount++; - if (httpClientGetCount === 1) { - // First long polling request must succeed so start completes - return ""; - } else { - throw new Error("fail"); - } - }) - .on("DELETE", () => new HttpResponse(202)), - } as IHttpConnectionOptions; + let httpClientGetCount = 0; + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => ({ connectionId: "42", availableTransports: [availableTransport] })) + .on("GET", () => { + httpClientGetCount++; + if (httpClientGetCount === 1) { + // First long polling request must succeed so start completes + return ""; + } + }) + .on("DELETE", () => new HttpResponse(202)), + logger, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); - try { - await connection.start(TransferFormat.Text); - expect(connection.features.inherentKeepAlive).toBe(true); - } finally { - await connection.stop(); - } + const connection = new HttpConnection("http://tempuri.org", options); + try { + await connection.start(TransferFormat.Text); + expect(connection.features.inherentKeepAlive).toBe(true); + } finally { + await connection.stop(); + } + }); }); it("does not select ServerSentEvents transport when not available in environment", async () => { - const serverSentEventsTransport = { transport: "ServerSentEvents", transferFormats: ["Text"] }; + await VerifyLogger.run(async (logger) => { + const serverSentEventsTransport = { transport: "ServerSentEvents", transferFormats: ["Text"] }; - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => ({ connectionId: "42", availableTransports: [serverSentEventsTransport] })), - } as IHttpConnectionOptions; + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => ({ connectionId: "42", availableTransports: [serverSentEventsTransport] })), + logger, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Unable to initialize any of the available transports."); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Unable to initialize any of the available transports."); + }, + "Failed to start the connection: Error: Unable to initialize any of the available transports."); }); it("does not select WebSockets transport when not available in environment", async () => { - const webSocketsTransport = { transport: "WebSockets", transferFormats: ["Text"] }; + await VerifyLogger.run(async (logger) => { + const webSocketsTransport = { transport: "WebSockets", transferFormats: ["Text"] }; - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", () => ({ connectionId: "42", availableTransports: [webSocketsTransport] })), - } as IHttpConnectionOptions; + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient() + .on("POST", () => ({ connectionId: "42", availableTransports: [webSocketsTransport] })), + logger, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Unable to initialize any of the available transports."); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Unable to initialize any of the available transports."); + }, + "Failed to start the connection: Error: Unable to initialize any of the available transports."); }); describe(".constructor", () => { @@ -580,129 +648,149 @@ describe("HttpConnection", () => { }); it("uses global WebSocket if defined", async () => { - // tslint:disable-next-line:no-string-literal - global["WebSocket"] = class WebSocket { - constructor() { - throw new Error("WebSocket constructor called."); - } - }; + await VerifyLogger.run(async (logger) => { + // tslint:disable-next-line:no-string-literal + global["WebSocket"] = class WebSocket { + constructor() { + throw new Error("WebSocket constructor called."); + } + }; - const options: IHttpConnectionOptions = { - ...commonOptions, - skipNegotiation: true, - transport: HttpTransportType.WebSockets, - } as IHttpConnectionOptions; + const options: IHttpConnectionOptions = { + ...commonOptions, + logger, + skipNegotiation: true, + transport: HttpTransportType.WebSockets, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start()) - .rejects - .toThrow("WebSocket constructor called."); + await expect(connection.start()) + .rejects + .toThrow("WebSocket constructor called."); - // tslint:disable-next-line:no-string-literal - delete global["WebSocket"]; + // tslint:disable-next-line:no-string-literal + delete global["WebSocket"]; + }, + "Failed to start the connection: Error: WebSocket constructor called."); }); it("uses global EventSource if defined", async () => { - let eventSourceConstructorCalled: boolean = false; - // tslint:disable-next-line:no-string-literal - global["EventSource"] = class EventSource { - constructor() { - eventSourceConstructorCalled = true; - throw new Error("EventSource constructor called."); - } - }; + await VerifyLogger.run(async (logger) => { + let eventSourceConstructorCalled: boolean = false; + // tslint:disable-next-line:no-string-literal + global["EventSource"] = class EventSource { + constructor() { + eventSourceConstructorCalled = true; + throw new Error("EventSource constructor called."); + } + }; - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient().on("POST", () => { - return { - availableTransports: [ - { transport: "ServerSentEvents", transferFormats: ["Text"] }, - ], - connectionId: defaultConnectionId, - }; - }), - transport: HttpTransportType.ServerSentEvents, - } as IHttpConnectionOptions; + const options: IHttpConnectionOptions = { + ...commonOptions, + httpClient: new TestHttpClient().on("POST", () => { + return { + availableTransports: [ + { transport: "ServerSentEvents", transferFormats: ["Text"] }, + ], + connectionId: defaultConnectionId, + }; + }), + logger, + transport: HttpTransportType.ServerSentEvents, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Unable to initialize any of the available transports."); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Unable to initialize any of the available transports."); - expect(eventSourceConstructorCalled).toEqual(true); + expect(eventSourceConstructorCalled).toEqual(true); - // tslint:disable-next-line:no-string-literal - delete global["EventSource"]; + // tslint:disable-next-line:no-string-literal + delete global["EventSource"]; + }, + "Failed to start the transport 'ServerSentEvents': Error: EventSource constructor called.", + "Failed to start the connection: Error: Unable to initialize any of the available transports."); }); it("uses EventSource constructor from options if provided", async () => { - let eventSourceConstructorCalled: boolean = false; + await VerifyLogger.run(async (logger) => { + let eventSourceConstructorCalled: boolean = false; - class TestEventSource { - // The "_" prefix tell TypeScript not to worry about unused parameter, but tslint doesn't like it. - // tslint:disable-next-line:variable-name - constructor(_url: string, _eventSourceInitDict: EventSourceInit) { - eventSourceConstructorCalled = true; - throw new Error("EventSource constructor called."); + class TestEventSource { + // The "_" prefix tell TypeScript not to worry about unused parameter, but tslint doesn't like it. + // tslint:disable-next-line:variable-name + constructor(_url: string, _eventSourceInitDict: EventSourceInit) { + eventSourceConstructorCalled = true; + throw new Error("EventSource constructor called."); + } } - } - const options: IHttpConnectionOptions = { - ...commonOptions, - EventSource: TestEventSource as EventSourceConstructor, - httpClient: new TestHttpClient().on("POST", () => { - return { - availableTransports: [ - { transport: "ServerSentEvents", transferFormats: ["Text"] }, - ], - connectionId: defaultConnectionId, - }; - }), - transport: HttpTransportType.ServerSentEvents, - } as IHttpConnectionOptions; + const options: IHttpConnectionOptions = { + ...commonOptions, + EventSource: TestEventSource as EventSourceConstructor, + httpClient: new TestHttpClient().on("POST", () => { + return { + availableTransports: [ + { transport: "ServerSentEvents", transferFormats: ["Text"] }, + ], + connectionId: defaultConnectionId, + }; + }), + logger, + transport: HttpTransportType.ServerSentEvents, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start(TransferFormat.Text)) - .rejects - .toThrow("Unable to initialize any of the available transports."); + await expect(connection.start(TransferFormat.Text)) + .rejects + .toThrow("Unable to initialize any of the available transports."); - expect(eventSourceConstructorCalled).toEqual(true); + expect(eventSourceConstructorCalled).toEqual(true); + }, + "Failed to start the transport 'ServerSentEvents': Error: EventSource constructor called.", + "Failed to start the connection: Error: Unable to initialize any of the available transports."); }); it("uses WebSocket constructor from options if provided", async () => { - class TestWebSocket { - // The "_" prefix tell TypeScript not to worry about unused parameter, but tslint doesn't like it. - // tslint:disable-next-line:variable-name - constructor(_url: string, _protocols?: string | string[]) { - throw new Error("WebSocket constructor called."); + await VerifyLogger.run(async (logger) => { + class TestWebSocket { + // The "_" prefix tell TypeScript not to worry about unused parameter, but tslint doesn't like it. + // tslint:disable-next-line:variable-name + constructor(_url: string, _protocols?: string | string[]) { + throw new Error("WebSocket constructor called."); + } } - } - const options: IHttpConnectionOptions = { - ...commonOptions, - WebSocket: TestWebSocket, - skipNegotiation: true, - transport: HttpTransportType.WebSockets, - } as IHttpConnectionOptions; + const options: IHttpConnectionOptions = { + ...commonOptions, + WebSocket: TestWebSocket as WebSocketConstructor, + logger, + skipNegotiation: true, + transport: HttpTransportType.WebSockets, + } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", options); - await expect(connection.start()) - .rejects - .toThrow("WebSocket constructor called."); + await expect(connection.start()) + .rejects + .toThrow("WebSocket constructor called."); + }, + "Failed to start the connection: Error: WebSocket constructor called."); }); }); describe("startAsync", () => { it("throws if an unsupported TransferFormat is provided", async () => { - // Force TypeScript to let us call start incorrectly - const connection: any = new HttpConnection("http://tempuri.org", commonOptions); + await VerifyLogger.run(async (logger) => { + // Force TypeScript to let us call start incorrectly + const connection: any = new HttpConnection("http://tempuri.org", { ...commonOptions, logger }); - expect(() => connection.start(42)).toThrowError("Unknown transferFormat value: 42."); + expect(() => connection.start(42)).toThrowError("Unknown transferFormat value: 42."); + }); }); }); }); diff --git a/clients/ts/signalr/tests/HubConnection.test.ts b/clients/ts/signalr/tests/HubConnection.test.ts index 6c55cb8177..9910c621c4 100644 --- a/clients/ts/signalr/tests/HubConnection.test.ts +++ b/clients/ts/signalr/tests/HubConnection.test.ts @@ -11,6 +11,7 @@ import { NullLogger } from "../src/Loggers"; import { IStreamSubscriber } from "../src/Stream"; import { TextMessageFormat } from "../src/TextMessageFormat"; +import { VerifyLogger } from "./Common"; import { delay, PromiseSource } from "./Utils"; function createHubConnection(connection: IConnection, logger?: ILogger | null, protocol?: IHubProtocol | null) { @@ -21,975 +22,1057 @@ describe("HubConnection", () => { describe("start", () => { it("sends negotiation message", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - await hubConnection.start(); - expect(connection.sentData.length).toBe(1); - expect(JSON.parse(connection.sentData[0])).toEqual({ - protocol: "json", - version: 1, - }); - } finally { - await hubConnection.stop(); - } + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + await hubConnection.start(); + expect(connection.sentData.length).toBe(1); + expect(JSON.parse(connection.sentData[0])).toEqual({ + protocol: "json", + version: 1, + }); + } finally { + await hubConnection.stop(); + } + }); }); it("state connected", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - expect(hubConnection.state).toBe(HubConnectionState.Disconnected); - try { - await hubConnection.start(); - expect(hubConnection.state).toBe(HubConnectionState.Connected); - } finally { - await hubConnection.stop(); - } + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + expect(hubConnection.state).toBe(HubConnectionState.Disconnected); + try { + await hubConnection.start(); + expect(hubConnection.state).toBe(HubConnectionState.Connected); + } finally { + await hubConnection.stop(); + } + }); }); }); describe("ping", () => { it("automatically sends multiple pings", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); - hubConnection.keepAliveIntervalInMilliseconds = 5; + hubConnection.keepAliveIntervalInMilliseconds = 5; - try { - await hubConnection.start(); - await delay(500); + try { + await hubConnection.start(); + await delay(500); - const numPings = connection.sentData.filter((s) => JSON.parse(s).type === MessageType.Ping).length; - expect(numPings).toBeGreaterThanOrEqual(2); - } finally { - await hubConnection.stop(); - } + const numPings = connection.sentData.filter((s) => JSON.parse(s).type === MessageType.Ping).length; + expect(numPings).toBeGreaterThanOrEqual(2); + } finally { + await hubConnection.stop(); + } + }); }); }); describe("stop", () => { it("state disconnected", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - expect(hubConnection.state).toBe(HubConnectionState.Disconnected); - try { - await hubConnection.start(); - expect(hubConnection.state).toBe(HubConnectionState.Connected); - } finally { - await hubConnection.stop(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); expect(hubConnection.state).toBe(HubConnectionState.Disconnected); - } + try { + await hubConnection.start(); + expect(hubConnection.state).toBe(HubConnectionState.Connected); + } finally { + await hubConnection.stop(); + expect(hubConnection.state).toBe(HubConnectionState.Disconnected); + } + }); }); }); describe("send", () => { it("sends a non blocking invocation", async () => { - const connection = new TestConnection(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - // We don't actually care to wait for the send. - // tslint:disable-next-line:no-floating-promises - hubConnection.send("testMethod", "arg", 42) - .catch((_) => { }); // Suppress exception and unhandled promise rejection warning. + const hubConnection = createHubConnection(connection, logger); + try { + // We don't actually care to wait for the send. + // tslint:disable-next-line:no-floating-promises + hubConnection.send("testMethod", "arg", 42) + .catch((_) => { }); // Suppress exception and unhandled promise rejection warning. - // Verify the message is sent - expect(connection.sentData.length).toBe(1); - expect(JSON.parse(connection.sentData[0])).toEqual({ - arguments: [ - "arg", - 42, - ], - target: "testMethod", - type: MessageType.Invocation, - }); - } finally { - // Close the connection - await hubConnection.stop(); - } + // Verify the message is sent + expect(connection.sentData.length).toBe(1); + expect(JSON.parse(connection.sentData[0])).toEqual({ + arguments: [ + "arg", + 42, + ], + target: "testMethod", + type: MessageType.Invocation, + }); + } finally { + // Close the connection + await hubConnection.stop(); + } + }); }); }); describe("invoke", () => { it("sends an invocation", async () => { - const connection = new TestConnection(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - // We don't actually care to wait for the send. - // tslint:disable-next-line:no-floating-promises - hubConnection.invoke("testMethod", "arg", 42) - .catch((_) => { }); // Suppress exception and unhandled promise rejection warning. + const hubConnection = createHubConnection(connection, logger); + try { + // We don't actually care to wait for the send. + // tslint:disable-next-line:no-floating-promises + hubConnection.invoke("testMethod", "arg", 42) + .catch((_) => { }); // Suppress exception and unhandled promise rejection warning. - // Verify the message is sent - expect(connection.sentData.length).toBe(1); - expect(JSON.parse(connection.sentData[0])).toEqual({ - arguments: [ - "arg", - 42, - ], - invocationId: connection.lastInvocationId, - target: "testMethod", - type: MessageType.Invocation, - }); + // Verify the message is sent + expect(connection.sentData.length).toBe(1); + expect(JSON.parse(connection.sentData[0])).toEqual({ + arguments: [ + "arg", + 42, + ], + invocationId: connection.lastInvocationId, + target: "testMethod", + type: MessageType.Invocation, + }); - } finally { - // Close the connection - await hubConnection.stop(); - } + } finally { + // Close the connection + await hubConnection.stop(); + } + }); }); it("can process handshake from text", async () => { - let protocolCalled = false; + await VerifyLogger.run(async (logger) => { + let protocolCalled = false; - const mockProtocol = new TestProtocol(TransferFormat.Text); - mockProtocol.onreceive = (d) => { - protocolCalled = true; - }; + const mockProtocol = new TestProtocol(TransferFormat.Text); + mockProtocol.onreceive = (d) => { + protocolCalled = true; + }; - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection, null, mockProtocol); - try { - const data = "{}" + TextMessageFormat.RecordSeparator; + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger, mockProtocol); + try { + const data = "{}" + TextMessageFormat.RecordSeparator; - connection.receiveText(data); + connection.receiveText(data); - // message only contained handshake response - expect(protocolCalled).toEqual(false); - } finally { - await hubConnection.stop(); - } + // message only contained handshake response + expect(protocolCalled).toEqual(false); + } finally { + await hubConnection.stop(); + } + }); }); it("can process handshake from binary", async () => { - let protocolCalled = false; + await VerifyLogger.run(async (logger) => { + let protocolCalled = false; - const mockProtocol = new TestProtocol(TransferFormat.Binary); - mockProtocol.onreceive = (d) => { - protocolCalled = true; - }; + const mockProtocol = new TestProtocol(TransferFormat.Binary); + mockProtocol.onreceive = (d) => { + protocolCalled = true; + }; - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection, null, mockProtocol); - try { - // handshake response + message separator - const data = [0x7b, 0x7d, 0x1e]; + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger, mockProtocol); + try { + // handshake response + message separator + const data = [0x7b, 0x7d, 0x1e]; - connection.receiveBinary(new Uint8Array(data).buffer); + connection.receiveBinary(new Uint8Array(data).buffer); - // message only contained handshake response - expect(protocolCalled).toEqual(false); - } finally { - await hubConnection.stop(); - } + // message only contained handshake response + expect(protocolCalled).toEqual(false); + } finally { + await hubConnection.stop(); + } + }); }); it("can process handshake and additional messages from binary", async () => { - let receivedProcotolData: ArrayBuffer | undefined; + await VerifyLogger.run(async (logger) => { + let receivedProcotolData: ArrayBuffer | undefined; - const mockProtocol = new TestProtocol(TransferFormat.Binary); - mockProtocol.onreceive = (d) => receivedProcotolData = d as ArrayBuffer; + const mockProtocol = new TestProtocol(TransferFormat.Binary); + mockProtocol.onreceive = (d) => receivedProcotolData = d as ArrayBuffer; - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection, null, mockProtocol); - try { - // handshake response + message separator + message pack message - const data = [ - 0x7b, 0x7d, 0x1e, 0x65, 0x95, 0x03, 0x80, 0xa1, 0x30, 0x01, 0xd9, 0x5d, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x20, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, - 0x69, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, - 0x6e, 0x67, 0x20, 0x27, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x27, 0x20, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x6e, 0x2d, 0x73, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x66, 0x61, 0x73, 0x68, 0x69, 0x6f, 0x6e, 0x2e, - ]; + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger, mockProtocol); + try { + // handshake response + message separator + message pack message + const data = [ + 0x7b, 0x7d, 0x1e, 0x65, 0x95, 0x03, 0x80, 0xa1, 0x30, 0x01, 0xd9, 0x5d, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x20, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, + 0x69, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, + 0x6e, 0x67, 0x20, 0x27, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x27, 0x20, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x6e, 0x2d, 0x73, 0x74, 0x72, + 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x66, 0x61, 0x73, 0x68, 0x69, 0x6f, 0x6e, 0x2e, + ]; - connection.receiveBinary(new Uint8Array(data).buffer); + connection.receiveBinary(new Uint8Array(data).buffer); - // left over data is the message pack message - expect(receivedProcotolData!.byteLength).toEqual(102); - } finally { - await hubConnection.stop(); - } + // left over data is the message pack message + expect(receivedProcotolData!.byteLength).toEqual(102); + } finally { + await hubConnection.stop(); + } + }); }); it("can process handshake and additional messages from text", async () => { - let receivedProcotolData: string | undefined; + await VerifyLogger.run(async (logger) => { + let receivedProcotolData: string | undefined; - const mockProtocol = new TestProtocol(TransferFormat.Text); - mockProtocol.onreceive = (d) => receivedProcotolData = d as string; + const mockProtocol = new TestProtocol(TransferFormat.Text); + mockProtocol.onreceive = (d) => receivedProcotolData = d as string; - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection, null, mockProtocol); - try { - const data = "{}" + TextMessageFormat.RecordSeparator + "{\"type\":6}" + TextMessageFormat.RecordSeparator; + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger, mockProtocol); + try { + const data = "{}" + TextMessageFormat.RecordSeparator + "{\"type\":6}" + TextMessageFormat.RecordSeparator; - connection.receiveText(data); + connection.receiveText(data); - expect(receivedProcotolData).toEqual("{\"type\":6}" + TextMessageFormat.RecordSeparator); - } finally { - await hubConnection.stop(); - } + expect(receivedProcotolData).toEqual("{\"type\":6}" + TextMessageFormat.RecordSeparator); + } finally { + await hubConnection.stop(); + } + }); }); it("rejects the promise when an error is received", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - const invokePromise = hubConnection.invoke("testMethod", "arg", 42); + const invokePromise = hubConnection.invoke("testMethod", "arg", 42); - connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, error: "foo" }); + connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, error: "foo" }); - await expect(invokePromise).rejects.toThrow("foo"); - } finally { - await hubConnection.stop(); - } + await expect(invokePromise).rejects.toThrow("foo"); + } finally { + await hubConnection.stop(); + } + }); }); it("resolves the promise when a result is received", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - const invokePromise = hubConnection.invoke("testMethod", "arg", 42); + const invokePromise = hubConnection.invoke("testMethod", "arg", 42); - connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, result: "foo" }); + connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, result: "foo" }); - expect(await invokePromise).toBe("foo"); - } finally { - await hubConnection.stop(); - } + expect(await invokePromise).toBe("foo"); + } finally { + await hubConnection.stop(); + } + }); }); it("completes pending invocations when stopped", async () => { - const connection = new TestConnection(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); + const hubConnection = createHubConnection(connection, logger); - connection.receiveHandshakeResponse(); - - const invokePromise = hubConnection.invoke("testMethod"); - await hubConnection.stop(); - - await expect(invokePromise).rejects.toThrow("Invocation canceled due to connection being closed."); - }); - - it("completes pending invocations when connection is lost", async () => { - const connection = new TestConnection(); - - const hubConnection = createHubConnection(connection); - try { connection.receiveHandshakeResponse(); const invokePromise = hubConnection.invoke("testMethod"); - // Typically this would be called by the transport - connection.onclose!(new Error("Connection lost")); - - await expect(invokePromise).rejects.toThrow("Connection lost"); - } finally { await hubConnection.stop(); - } + + await expect(invokePromise).rejects.toThrow("Invocation canceled due to connection being closed."); + }); + }); + + it("completes pending invocations when connection is lost", async () => { + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); + + const invokePromise = hubConnection.invoke("testMethod"); + // Typically this would be called by the transport + connection.onclose!(new Error("Connection lost")); + + await expect(invokePromise).rejects.toThrow("Connection lost"); + } finally { + await hubConnection.stop(); + } + }); }); }); describe("on", () => { it("invocations ignored in callbacks not registered", async () => { - const warnings: string[] = []; - const logger = { - log: (logLevel: LogLevel, message: string) => { - if (logLevel === LogLevel.Warning) { - warnings.push(message); - } - }, - } as ILogger; - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection, logger); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const warnings: string[] = []; + const wrappingLogger = { + log: (logLevel: LogLevel, message: string) => { + if (logLevel === LogLevel.Warning) { + warnings.push(message); + } + logger.log(logLevel, message); + }, + } as ILogger; + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, wrappingLogger); + try { + connection.receiveHandshakeResponse(); - connection.receive({ - arguments: ["test"], - nonblocking: true, - target: "message", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: ["test"], + nonblocking: true, + target: "message", + type: MessageType.Invocation, + }); - expect(warnings).toEqual(["No client method with the name 'message' found."]); - } finally { - await hubConnection.stop(); - } + expect(warnings).toEqual(["No client method with the name 'message' found."]); + } finally { + await hubConnection.stop(); + } + }); }); it("invocations ignored in callbacks that have registered then unregistered", async () => { - const warnings: string[] = []; - const logger = { - log: (logLevel: LogLevel, message: string) => { - if (logLevel === LogLevel.Warning) { - warnings.push(message); - } - }, - } as ILogger; - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection, logger); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const warnings: string[] = []; + const wrappingLogger = { + log: (logLevel: LogLevel, message: string) => { + if (logLevel === LogLevel.Warning) { + warnings.push(message); + } + logger.log(logLevel, message); + }, + } as ILogger; + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, wrappingLogger); + try { + connection.receiveHandshakeResponse(); - const handler = () => { }; - hubConnection.on("message", handler); - hubConnection.off("message", handler); + const handler = () => { }; + hubConnection.on("message", handler); + hubConnection.off("message", handler); - connection.receive({ - arguments: ["test"], - invocationId: "0", - nonblocking: true, - target: "message", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: ["test"], + invocationId: "0", + nonblocking: true, + target: "message", + type: MessageType.Invocation, + }); - expect(warnings).toEqual(["No client method with the name 'message' found."]); - } finally { - await hubConnection.stop(); - } + expect(warnings).toEqual(["No client method with the name 'message' found."]); + } finally { + await hubConnection.stop(); + } + }); }); it("all handlers can be unregistered with just the method name", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - let count = 0; - const handler = () => { count++; }; - const secondHandler = () => { count++; }; - hubConnection.on("inc", handler); - hubConnection.on("inc", secondHandler); + let count = 0; + const handler = () => { count++; }; + const secondHandler = () => { count++; }; + hubConnection.on("inc", handler); + hubConnection.on("inc", secondHandler); - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "inc", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: [], + nonblocking: true, + target: "inc", + type: MessageType.Invocation, + }); - hubConnection.off("inc"); + hubConnection.off("inc"); - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "inc", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: [], + nonblocking: true, + target: "inc", + type: MessageType.Invocation, + }); - expect(count).toBe(2); - } finally { - await hubConnection.stop(); - } + expect(count).toBe(2); + } finally { + await hubConnection.stop(); + } + }); }); it("a single handler can be unregistered with the method name and handler", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - let count = 0; - const handler = () => { count++; }; - const secondHandler = () => { count++; }; - hubConnection.on("inc", handler); - hubConnection.on("inc", secondHandler); + let count = 0; + const handler = () => { count++; }; + const secondHandler = () => { count++; }; + hubConnection.on("inc", handler); + hubConnection.on("inc", secondHandler); - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "inc", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: [], + nonblocking: true, + target: "inc", + type: MessageType.Invocation, + }); - hubConnection.off("inc", handler); + hubConnection.off("inc", handler); - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "inc", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: [], + nonblocking: true, + target: "inc", + type: MessageType.Invocation, + }); - expect(count).toBe(3); - } finally { - await hubConnection.stop(); - } + expect(count).toBe(3); + } finally { + await hubConnection.stop(); + } + }); }); it("can't register the same handler multiple times", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - let count = 0; - const handler = () => { count++; }; - hubConnection.on("inc", handler); - hubConnection.on("inc", handler); + let count = 0; + const handler = () => { count++; }; + hubConnection.on("inc", handler); + hubConnection.on("inc", handler); - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "inc", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: [], + nonblocking: true, + target: "inc", + type: MessageType.Invocation, + }); - expect(count).toBe(1); - } finally { - await hubConnection.stop(); - } + expect(count).toBe(1); + } finally { + await hubConnection.stop(); + } + }); }); it("callback invoked when servers invokes a method on the client", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - let value = ""; - hubConnection.on("message", (v) => value = v); + let value = ""; + hubConnection.on("message", (v) => value = v); - connection.receive({ - arguments: ["test"], - invocationId: "0", - nonblocking: true, - target: "message", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: ["test"], + nonblocking: true, + target: "message", + type: MessageType.Invocation, + }); - expect(value).toBe("test"); - } finally { - await hubConnection.stop(); - } + expect(value).toBe("test"); + } finally { + await hubConnection.stop(); + } + }); }); it("stop on handshake error", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - let closeError: Error | undefined; - hubConnection.onclose((e) => closeError = e); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + let closeError: Error | undefined; + hubConnection.onclose((e) => closeError = e); - connection.receiveHandshakeResponse("Error!"); + connection.receiveHandshakeResponse("Error!"); - expect(closeError!.message).toEqual("Server returned handshake error: Error!"); - } finally { - await hubConnection.stop(); - } + expect(closeError!.message).toEqual("Server returned handshake error: Error!"); + } finally { + await hubConnection.stop(); + } + }, + "Server returned handshake error: Error!"); }); it("stop on close message", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - let isClosed = false; - let closeError: Error | undefined; - hubConnection.onclose((e) => { - isClosed = true; - closeError = e; - }); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + let isClosed = false; + let closeError: Error | undefined; + hubConnection.onclose((e) => { + isClosed = true; + closeError = e; + }); - connection.receiveHandshakeResponse(); + connection.receiveHandshakeResponse(); - connection.receive({ - type: MessageType.Close, - }); + connection.receive({ + type: MessageType.Close, + }); - expect(isClosed).toEqual(true); - expect(closeError).toBeUndefined(); - } finally { - await hubConnection.stop(); - } + expect(isClosed).toEqual(true); + expect(closeError).toBeUndefined(); + } finally { + await hubConnection.stop(); + } + }); }); it("stop on error close message", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - let isClosed = false; - let closeError: Error | undefined; - hubConnection.onclose((e) => { - isClosed = true; - closeError = e; - }); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + let isClosed = false; + let closeError: Error | undefined; + hubConnection.onclose((e) => { + isClosed = true; + closeError = e; + }); - connection.receiveHandshakeResponse(); + connection.receiveHandshakeResponse(); - connection.receive({ - error: "Error!", - type: MessageType.Close, - }); + connection.receive({ + error: "Error!", + type: MessageType.Close, + }); - expect(isClosed).toEqual(true); - expect(closeError!.message).toEqual("Server returned an error on close: Error!"); - } finally { - await hubConnection.stop(); - } + expect(isClosed).toEqual(true); + expect(closeError!.message).toEqual("Server returned an error on close: Error!"); + } finally { + await hubConnection.stop(); + } + }); }); it("can have multiple callbacks", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - let numInvocations1 = 0; - let numInvocations2 = 0; - hubConnection.on("message", () => numInvocations1++); - hubConnection.on("message", () => numInvocations2++); + let numInvocations1 = 0; + let numInvocations2 = 0; + hubConnection.on("message", () => numInvocations1++); + hubConnection.on("message", () => numInvocations2++); - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "message", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: [], + nonblocking: true, + target: "message", + type: MessageType.Invocation, + }); - expect(numInvocations1).toBe(1); - expect(numInvocations2).toBe(1); - } finally { - await hubConnection.stop(); - } + expect(numInvocations1).toBe(1); + expect(numInvocations2).toBe(1); + } finally { + await hubConnection.stop(); + } + }); }); it("can unsubscribe from on", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - let numInvocations = 0; - const callback = () => numInvocations++; - hubConnection.on("message", callback); + let numInvocations = 0; + const callback = () => numInvocations++; + hubConnection.on("message", callback); - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "message", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: [], + nonblocking: true, + target: "message", + type: MessageType.Invocation, + }); - hubConnection.off("message", callback); + hubConnection.off("message", callback); - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "message", - type: MessageType.Invocation, - }); + connection.receive({ + arguments: [], + nonblocking: true, + target: "message", + type: MessageType.Invocation, + }); - expect(numInvocations).toBe(1); - } finally { - await hubConnection.stop(); - } + expect(numInvocations).toBe(1); + } finally { + await hubConnection.stop(); + } + }); }); it("unsubscribing from non-existing callbacks no-ops", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - hubConnection.off("_", () => { }); - hubConnection.on("message", (t) => { }); - hubConnection.on("message", () => { }); - } finally { - await hubConnection.stop(); - } + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + hubConnection.off("_", () => { }); + hubConnection.on("message", (t) => { }); + hubConnection.on("message", () => { }); + } finally { + await hubConnection.stop(); + } + }); }); it("using null/undefined for methodName or method no-ops", async () => { - const warnings: string[] = []; - const logger = { - log(logLevel: LogLevel, message: string) { - if (logLevel === LogLevel.Warning) { - warnings.push(message); - } + await VerifyLogger.run(async (logger) => { + const warnings: string[] = []; + const wrappingLogger = { + log(logLevel: LogLevel, message: string) { + if (logLevel === LogLevel.Warning) { + warnings.push(message); + } + logger.log(logLevel, message); + }, + } as ILogger; - }, - } as ILogger; + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, wrappingLogger); + try { + connection.receiveHandshakeResponse(); - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection, logger); - try { - connection.receiveHandshakeResponse(); + hubConnection.on(null!, undefined!); + hubConnection.on(undefined!, null!); + hubConnection.on("message", null!); + hubConnection.on("message", undefined!); + hubConnection.on(null!, () => { }); + hubConnection.on(undefined!, () => { }); - hubConnection.on(null!, undefined!); - hubConnection.on(undefined!, null!); - hubConnection.on("message", null!); - hubConnection.on("message", undefined!); - hubConnection.on(null!, () => { }); - hubConnection.on(undefined!, () => { }); + // invoke a method to make sure we are not trying to use null/undefined + connection.receive({ + arguments: [], + invocationId: "0", + nonblocking: true, + target: "message", + type: MessageType.Invocation, + }); - // invoke a method to make sure we are not trying to use null/undefined - connection.receive({ - arguments: [], - invocationId: "0", - nonblocking: true, - target: "message", - type: MessageType.Invocation, - }); + expect(warnings).toEqual(["No client method with the name 'message' found."]); - expect(warnings).toEqual(["No client method with the name 'message' found."]); - - hubConnection.off(null!, undefined!); - hubConnection.off(undefined!, null!); - hubConnection.off("message", null!); - hubConnection.off("message", undefined!); - hubConnection.off(null!, () => { }); - hubConnection.off(undefined!, () => { }); - } finally { - await hubConnection.stop(); - } + hubConnection.off(null!, undefined!); + hubConnection.off(undefined!, null!); + hubConnection.off("message", null!); + hubConnection.off("message", undefined!); + hubConnection.off(null!, () => { }); + hubConnection.off(undefined!, () => { }); + } finally { + await hubConnection.stop(); + } + }); }); }); describe("stream", () => { it("sends an invocation", async () => { - const connection = new TestConnection(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - hubConnection.stream("testStream", "arg", 42); + const hubConnection = createHubConnection(connection, logger); + try { + hubConnection.stream("testStream", "arg", 42); - // Verify the message is sent - expect(connection.sentData.length).toBe(1); - expect(JSON.parse(connection.sentData[0])).toEqual({ - arguments: [ - "arg", - 42, - ], - invocationId: connection.lastInvocationId, - target: "testStream", - type: MessageType.StreamInvocation, - }); + // Verify the message is sent + expect(connection.sentData.length).toBe(1); + expect(JSON.parse(connection.sentData[0])).toEqual({ + arguments: [ + "arg", + 42, + ], + invocationId: connection.lastInvocationId, + target: "testStream", + type: MessageType.StreamInvocation, + }); - // Close the connection - await hubConnection.stop(); - } finally { - await hubConnection.stop(); - } + // Close the connection + await hubConnection.stop(); + } finally { + await hubConnection.stop(); + } + }); }); it("completes with an error when an error is yielded", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - const observer = new TestObserver(); - hubConnection.stream("testMethod", "arg", 42) - .subscribe(observer); + const observer = new TestObserver(); + hubConnection.stream("testMethod", "arg", 42) + .subscribe(observer); - connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, error: "foo" }); + connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, error: "foo" }); - await expect(observer.completed).rejects.toThrow("Error: foo"); - } finally { - await hubConnection.stop(); - } + await expect(observer.completed).rejects.toThrow("Error: foo"); + } finally { + await hubConnection.stop(); + } + }); }); it("completes the observer when a completion is received", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - const observer = new TestObserver(); - hubConnection.stream("testMethod", "arg", 42) - .subscribe(observer); + const observer = new TestObserver(); + hubConnection.stream("testMethod", "arg", 42) + .subscribe(observer); - connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId }); + connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId }); - expect(await observer.completed).toEqual([]); - } finally { - await hubConnection.stop(); - } + expect(await observer.completed).toEqual([]); + } finally { + await hubConnection.stop(); + } + }); }); it("completes pending streams when stopped", async () => { - const connection = new TestConnection(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - const observer = new TestObserver(); - hubConnection.stream("testMethod") - .subscribe(observer); - await hubConnection.stop(); + const hubConnection = createHubConnection(connection, logger); + try { + const observer = new TestObserver(); + hubConnection.stream("testMethod") + .subscribe(observer); + await hubConnection.stop(); - await expect(observer.completed).rejects.toThrow("Error: Invocation canceled due to connection being closed."); - } finally { - await hubConnection.stop(); - } + await expect(observer.completed).rejects.toThrow("Error: Invocation canceled due to connection being closed."); + } finally { + await hubConnection.stop(); + } + }); }); it("completes pending streams when connection is lost", async () => { - const connection = new TestConnection(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - const observer = new TestObserver(); - hubConnection.stream("testMethod") - .subscribe(observer); + const hubConnection = createHubConnection(connection, logger); + try { + const observer = new TestObserver(); + hubConnection.stream("testMethod") + .subscribe(observer); - // Typically this would be called by the transport - connection.onclose!(new Error("Connection lost")); + // Typically this would be called by the transport + connection.onclose!(new Error("Connection lost")); - await expect(observer.completed).rejects.toThrow("Error: Connection lost"); - } finally { - await hubConnection.stop(); - } + await expect(observer.completed).rejects.toThrow("Error: Connection lost"); + } finally { + await hubConnection.stop(); + } + }); }); it("yields items as they arrive", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - const observer = new TestObserver(); - hubConnection.stream("testMethod") - .subscribe(observer); + const observer = new TestObserver(); + hubConnection.stream("testMethod") + .subscribe(observer); - connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 1 }); - expect(observer.itemsReceived).toEqual([1]); + connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 1 }); + expect(observer.itemsReceived).toEqual([1]); - connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 2 }); - expect(observer.itemsReceived).toEqual([1, 2]); + connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 2 }); + expect(observer.itemsReceived).toEqual([1, 2]); - connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 3 }); - expect(observer.itemsReceived).toEqual([1, 2, 3]); + connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 3 }); + expect(observer.itemsReceived).toEqual([1, 2, 3]); - connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId }); - expect(await observer.completed).toEqual([1, 2, 3]); - } finally { - await hubConnection.stop(); - } + connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId }); + expect(await observer.completed).toEqual([1, 2, 3]); + } finally { + await hubConnection.stop(); + } + }); }); it("does not require error function registered", async () => { - const connection = new TestConnection(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - hubConnection.stream("testMethod").subscribe(NullSubscriber.instance); + const hubConnection = createHubConnection(connection, logger); + try { + hubConnection.stream("testMethod").subscribe(NullSubscriber.instance); - // Typically this would be called by the transport - // triggers observer.error() - connection.onclose!(new Error("Connection lost")); - } finally { - await hubConnection.stop(); - } + // Typically this would be called by the transport + // triggers observer.error() + connection.onclose!(new Error("Connection lost")); + } finally { + await hubConnection.stop(); + } + }); }); it("does not require complete function registered", async () => { - const connection = new TestConnection(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - hubConnection.stream("testMethod").subscribe(NullSubscriber.instance); + const hubConnection = createHubConnection(connection, logger); + try { + hubConnection.stream("testMethod").subscribe(NullSubscriber.instance); - // Send completion to trigger observer.complete() - // Expectation is connection.receive will not to throw - connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId }); - } finally { - await hubConnection.stop(); - } + // Send completion to trigger observer.complete() + // Expectation is connection.receive will not to throw + connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId }); + } finally { + await hubConnection.stop(); + } + }); }); it("can be canceled", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - connection.receiveHandshakeResponse(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + connection.receiveHandshakeResponse(); - const observer = new TestObserver(); - const subscription = hubConnection.stream("testMethod") - .subscribe(observer); + const observer = new TestObserver(); + const subscription = hubConnection.stream("testMethod") + .subscribe(observer); - connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 1 }); - expect(observer.itemsReceived).toEqual([1]); + connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 1 }); + expect(observer.itemsReceived).toEqual([1]); - subscription.dispose(); + subscription.dispose(); - connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 2 }); - // Observer should no longer receive messages - expect(observer.itemsReceived).toEqual([1]); + connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 2 }); + // Observer should no longer receive messages + expect(observer.itemsReceived).toEqual([1]); - // Verify the cancel is sent - expect(connection.sentData.length).toBe(2); - expect(JSON.parse(connection.sentData[1])).toEqual({ - invocationId: connection.lastInvocationId, - type: MessageType.CancelInvocation, - }); - } finally { - await hubConnection.stop(); - } + // Verify the cancel is sent + expect(connection.sentData.length).toBe(2); + expect(JSON.parse(connection.sentData[1])).toEqual({ + invocationId: connection.lastInvocationId, + type: MessageType.CancelInvocation, + }); + } finally { + await hubConnection.stop(); + } + }); }); }); describe("onClose", () => { it("can have multiple callbacks", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - let invocations = 0; - hubConnection.onclose((e) => invocations++); - hubConnection.onclose((e) => invocations++); - // Typically this would be called by the transport - connection.onclose!(); - expect(invocations).toBe(2); - } finally { - await hubConnection.stop(); - } + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + let invocations = 0; + hubConnection.onclose((e) => invocations++); + hubConnection.onclose((e) => invocations++); + // Typically this would be called by the transport + connection.onclose!(); + expect(invocations).toBe(2); + } finally { + await hubConnection.stop(); + } + }); }); it("callbacks receive error", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - let error: Error | undefined; - hubConnection.onclose((e) => error = e); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + let error: Error | undefined; + hubConnection.onclose((e) => error = e); - // Typically this would be called by the transport - connection.onclose!(new Error("Test error.")); - expect(error!.message).toBe("Test error."); - } finally { - await hubConnection.stop(); - } + // Typically this would be called by the transport + connection.onclose!(new Error("Test error.")); + expect(error!.message).toBe("Test error."); + } finally { + await hubConnection.stop(); + } + }); }); it("ignores null callbacks", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - hubConnection.onclose(null!); - hubConnection.onclose(undefined!); - // Typically this would be called by the transport - connection.onclose!(); - // expect no errors - } finally { - await hubConnection.stop(); - } + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + hubConnection.onclose(null!); + hubConnection.onclose(undefined!); + // Typically this would be called by the transport + connection.onclose!(); + // expect no errors + } finally { + await hubConnection.stop(); + } + }); }); it("state disconnected", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - let state: HubConnectionState | undefined; - hubConnection.onclose((e) => state = hubConnection.state); - // Typically this would be called by the transport - connection.onclose!(); + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + let state: HubConnectionState | undefined; + hubConnection.onclose((e) => state = hubConnection.state); + // Typically this would be called by the transport + connection.onclose!(); - expect(state).toBe(HubConnectionState.Disconnected); - } finally { - await hubConnection.stop(); - } + expect(state).toBe(HubConnectionState.Disconnected); + } finally { + await hubConnection.stop(); + } + }); }); }); describe("keepAlive", () => { it("can receive ping messages", async () => { - // Receive the ping mid-invocation so we can see that the rest of the flow works fine + await VerifyLogger.run(async (logger) => { + // Receive the ping mid-invocation so we can see that the rest of the flow works fine - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - const invokePromise = hubConnection.invoke("testMethod", "arg", 42); + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + const invokePromise = hubConnection.invoke("testMethod", "arg", 42); - connection.receive({ type: MessageType.Ping }); - connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, result: "foo" }); + connection.receive({ type: MessageType.Ping }); + connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, result: "foo" }); - expect(await invokePromise).toBe("foo"); - } finally { - await hubConnection.stop(); - } + expect(await invokePromise).toBe("foo"); + } finally { + await hubConnection.stop(); + } + }); }); it("does not terminate if messages are received", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - hubConnection.serverTimeoutInMilliseconds = 200; + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + hubConnection.serverTimeoutInMilliseconds = 200; - const p = new PromiseSource(); - hubConnection.onclose((e) => p.resolve(e)); + const p = new PromiseSource(); + hubConnection.onclose((e) => p.resolve(e)); - await hubConnection.start(); + await hubConnection.start(); - for (let i = 0; i < 6; i++) { - await pingAndWait(connection); + for (let i = 0; i < 6; i++) { + await pingAndWait(connection); + } + + await connection.stop(); + + const error = await p.promise; + + expect(error).toBeUndefined(); + } finally { + await hubConnection.stop(); } - - await connection.stop(); - - const error = await p.promise; - - expect(error).toBeUndefined(); - } finally { - await hubConnection.stop(); - } + }); }); it("does not timeout if message was received before HubConnection.start", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - hubConnection.serverTimeoutInMilliseconds = 200; + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + hubConnection.serverTimeoutInMilliseconds = 200; - const p = new PromiseSource(); - hubConnection.onclose((e) => p.resolve(e)); + const p = new PromiseSource(); + hubConnection.onclose((e) => p.resolve(e)); - // send message before start to trigger timeout handler - // testing for regression where we didn't cleanup timer if request received before start created a timer - await connection.receive({ type: MessageType.Ping }); + // send message before start to trigger timeout handler + // testing for regression where we didn't cleanup timer if request received before start created a timer + await connection.receive({ type: MessageType.Ping }); - await hubConnection.start(); + await hubConnection.start(); - for (let i = 0; i < 6; i++) { - await pingAndWait(connection); + for (let i = 0; i < 6; i++) { + await pingAndWait(connection); + } + + await connection.stop(); + + const error = await p.promise; + + expect(error).toBeUndefined(); + } finally { + await hubConnection.stop(); } - - await connection.stop(); - - const error = await p.promise; - - expect(error).toBeUndefined(); - } finally { - await hubConnection.stop(); - } + }); }); it("terminates if no messages received within timeout interval", async () => { - const connection = new TestConnection(); - const hubConnection = createHubConnection(connection); - try { - hubConnection.serverTimeoutInMilliseconds = 100; + await VerifyLogger.run(async (logger) => { + const connection = new TestConnection(); + const hubConnection = createHubConnection(connection, logger); + try { + hubConnection.serverTimeoutInMilliseconds = 100; - const p = new PromiseSource(); - hubConnection.onclose((e) => p.resolve(e)); + const p = new PromiseSource(); + hubConnection.onclose((e) => p.resolve(e)); - await hubConnection.start(); + await hubConnection.start(); - const error = await p.promise; + const error = await p.promise; - expect(error).toEqual(new Error("Server timeout elapsed without receiving a message from the server.")); - } finally { - await hubConnection.stop(); - } + expect(error).toEqual(new Error("Server timeout elapsed without receiving a message from the server.")); + } finally { + await hubConnection.stop(); + } + }); }); }); }); diff --git a/clients/ts/signalr/tests/HubConnectionBuilder.test.ts b/clients/ts/signalr/tests/HubConnectionBuilder.test.ts index 8c76e36a7c..3149fec14f 100644 --- a/clients/ts/signalr/tests/HubConnectionBuilder.test.ts +++ b/clients/ts/signalr/tests/HubConnectionBuilder.test.ts @@ -10,6 +10,7 @@ import { ILogger, LogLevel } from "../src/ILogger"; import { HttpTransportType, TransferFormat } from "../src/ITransport"; import { NullLogger } from "../src/Loggers"; +import { VerifyLogger } from "./Common"; import { TestHttpClient } from "./TestHttpClient"; import { PromiseSource } from "./Utils"; @@ -43,29 +44,32 @@ describe("HubConnectionBuilder", () => { }); it("builds HubConnection with HttpConnection using provided URL", async () => { - const pollSent = new PromiseSource(); - const pollCompleted = new PromiseSource(); - const testClient = createTestClient(pollSent, pollCompleted.promise) - .on("POST", "http://example.com?id=abc123", (req) => { - // Respond from the poll with the handshake response - pollCompleted.resolve(new HttpResponse(204, "No Content", "{}")); - return new HttpResponse(202); - }); - const connection = createConnectionBuilder() - .withUrl("http://example.com", { - ...commonHttpOptions, - httpClient: testClient, - }) - .build(); + await VerifyLogger.run(async (logger) => { + const pollSent = new PromiseSource(); + const pollCompleted = new PromiseSource(); + const testClient = createTestClient(pollSent, pollCompleted.promise) + .on("POST", "http://example.com?id=abc123", (req) => { + // Respond from the poll with the handshake response + pollCompleted.resolve(new HttpResponse(204, "No Content", "{}")); + return new HttpResponse(202); + }); + const connection = createConnectionBuilder() + .withUrl("http://example.com", { + ...commonHttpOptions, + httpClient: testClient, + logger, + }) + .build(); - // Start the connection - const closed = makeClosedPromise(connection); - await connection.start(); + // Start the connection + const closed = makeClosedPromise(connection); + await connection.start(); - const pollRequest = await pollSent.promise; - expect(pollRequest.url).toMatch(/http:\/\/example.com\?id=abc123.*/); + const pollRequest = await pollSent.promise; + expect(pollRequest.url).toMatch(/http:\/\/example.com\?id=abc123.*/); - await closed; + await closed; + }); }); it("can configure transport type", async () => { @@ -78,35 +82,38 @@ describe("HubConnectionBuilder", () => { }); it("can configure hub protocol", async () => { - const protocol = new TestProtocol(); + await VerifyLogger.run(async (logger) => { + const protocol = new TestProtocol(); - const pollSent = new PromiseSource(); - const pollCompleted = new PromiseSource(); - const negotiateReceived = new PromiseSource(); - const testClient = createTestClient(pollSent, pollCompleted.promise) - .on("POST", "http://example.com?id=abc123", (req) => { - // Respond from the poll with the handshake response - negotiateReceived.resolve(req); - pollCompleted.resolve(new HttpResponse(204, "No Content", "{}")); - return new HttpResponse(202); - }); + const pollSent = new PromiseSource(); + const pollCompleted = new PromiseSource(); + const negotiateReceived = new PromiseSource(); + const testClient = createTestClient(pollSent, pollCompleted.promise) + .on("POST", "http://example.com?id=abc123", (req) => { + // Respond from the poll with the handshake response + negotiateReceived.resolve(req); + pollCompleted.resolve(new HttpResponse(204, "No Content", "{}")); + return new HttpResponse(202); + }); - const connection = createConnectionBuilder() - .withUrl("http://example.com", { - ...commonHttpOptions, - httpClient: testClient, - }) - .withHubProtocol(protocol) - .build(); + const connection = createConnectionBuilder() + .withUrl("http://example.com", { + ...commonHttpOptions, + httpClient: testClient, + logger, + }) + .withHubProtocol(protocol) + .build(); - // Start the connection - const closed = makeClosedPromise(connection); - await connection.start(); + // Start the connection + const closed = makeClosedPromise(connection); + await connection.start(); - const negotiateRequest = await negotiateReceived.promise; - expect(negotiateRequest.content).toBe(`{"protocol":"${protocol.name}","version":1}\x1E`); + const negotiateRequest = await negotiateReceived.promise; + expect(negotiateRequest.content).toBe(`{"protocol":"${protocol.name}","version":1}\x1E`); - await closed; + await closed; + }); }); it("allows logger to be replaced", async () => { diff --git a/clients/ts/signalr/tests/JsonHubProtocol.test.ts b/clients/ts/signalr/tests/JsonHubProtocol.test.ts index 61d9868905..602fec2ddf 100644 --- a/clients/ts/signalr/tests/JsonHubProtocol.test.ts +++ b/clients/ts/signalr/tests/JsonHubProtocol.test.ts @@ -3,63 +3,71 @@ import { CompletionMessage, InvocationMessage, MessageType, StreamItemMessage } from "../src/IHubProtocol"; import { JsonHubProtocol } from "../src/JsonHubProtocol"; -import { NullLogger } from "../src/Loggers"; import { TextMessageFormat } from "../src/TextMessageFormat"; +import { VerifyLogger } from "./Common"; describe("JsonHubProtocol", () => { - it("can write/read non-blocking Invocation message", () => { - const invocation = { - arguments: [42, true, "test", ["x1", "y2"], null], - headers: {}, - target: "myMethod", - type: MessageType.Invocation, - } as InvocationMessage; + it("can write/read non-blocking Invocation message", async () => { + await VerifyLogger.run(async (logger) => { + const invocation = { + arguments: [42, true, "test", ["x1", "y2"], null], + headers: {}, + target: "myMethod", + type: MessageType.Invocation, + } as InvocationMessage; - const protocol = new JsonHubProtocol(); - const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), NullLogger.instance); - expect(parsedMessages).toEqual([invocation]); + const protocol = new JsonHubProtocol(); + const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), logger); + expect(parsedMessages).toEqual([invocation]); + }); }); - it("can read Invocation message with Date argument", () => { - const invocation = { - arguments: [Date.UTC(2018, 1, 1, 12, 34, 56)], - headers: {}, - target: "mymethod", - type: MessageType.Invocation, - } as InvocationMessage; + it("can read Invocation message with Date argument", async () => { + await VerifyLogger.run(async (logger) => { + const invocation = { + arguments: [Date.UTC(2018, 1, 1, 12, 34, 56)], + headers: {}, + target: "mymethod", + type: MessageType.Invocation, + } as InvocationMessage; - const protocol = new JsonHubProtocol(); - const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), NullLogger.instance); - expect(parsedMessages).toEqual([invocation]); + const protocol = new JsonHubProtocol(); + const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), logger); + expect(parsedMessages).toEqual([invocation]); + }); }); - it("can write/read Invocation message with headers", () => { - const invocation = { - arguments: [42, true, "test", ["x1", "y2"], null], - headers: { - foo: "bar", - }, - target: "myMethod", - type: MessageType.Invocation, - } as InvocationMessage; + it("can write/read Invocation message with headers", async () => { + await VerifyLogger.run(async (logger) => { + const invocation = { + arguments: [42, true, "test", ["x1", "y2"], null], + headers: { + foo: "bar", + }, + target: "myMethod", + type: MessageType.Invocation, + } as InvocationMessage; - const protocol = new JsonHubProtocol(); - const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), NullLogger.instance); - expect(parsedMessages).toEqual([invocation]); + const protocol = new JsonHubProtocol(); + const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), logger); + expect(parsedMessages).toEqual([invocation]); + }); }); - it("can write/read Invocation message", () => { - const invocation = { - arguments: [42, true, "test", ["x1", "y2"], null], - headers: {}, - invocationId: "123", - target: "myMethod", - type: MessageType.Invocation, - } as InvocationMessage; + it("can write/read Invocation message", async () => { + await VerifyLogger.run(async (logger) => { + const invocation = { + arguments: [42, true, "test", ["x1", "y2"], null], + headers: {}, + invocationId: "123", + target: "myMethod", + type: MessageType.Invocation, + } as InvocationMessage; - const protocol = new JsonHubProtocol(); - const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), NullLogger.instance); - expect(parsedMessages).toEqual([invocation]); + const protocol = new JsonHubProtocol(); + const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), logger); + expect(parsedMessages).toEqual([invocation]); + }); }); ([ @@ -101,9 +109,11 @@ describe("JsonHubProtocol", () => { type: MessageType.Completion, } as CompletionMessage], ] as Array<[string, CompletionMessage]>).forEach(([payload, expectedMessage]) => - it("can read Completion message", () => { - const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); - expect(messages).toEqual([expectedMessage]); + it("can read Completion message", async () => { + await VerifyLogger.run(async (logger) => { + const messages = new JsonHubProtocol().parseMessages(payload, logger); + expect(messages).toEqual([expectedMessage]); + }); })); ([ @@ -122,9 +132,11 @@ describe("JsonHubProtocol", () => { type: MessageType.StreamItem, } as StreamItemMessage], ] as Array<[string, StreamItemMessage]>).forEach(([payload, expectedMessage]) => - it("can read StreamItem message", () => { - const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); - expect(messages).toEqual([expectedMessage]); + it("can read StreamItem message", async () => { + await VerifyLogger.run(async (logger) => { + const messages = new JsonHubProtocol().parseMessages(payload, logger); + expect(messages).toEqual([expectedMessage]); + }); })); ([ @@ -138,9 +150,11 @@ describe("JsonHubProtocol", () => { type: MessageType.StreamItem, } as StreamItemMessage], ] as Array<[string, StreamItemMessage]>).forEach(([payload, expectedMessage]) => - it("can read message with headers", () => { - const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); - expect(messages).toEqual([expectedMessage]); + it("can read message with headers", async () => { + await VerifyLogger.run(async (logger) => { + const messages = new JsonHubProtocol().parseMessages(payload, logger); + expect(messages).toEqual([expectedMessage]); + }); })); ([ @@ -155,37 +169,43 @@ describe("JsonHubProtocol", () => { ["Completion message with result and error", `{"type":3,"invocationId":"1","result":2,"error":"error"}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Completion message."], ["Completion message with non-string error", `{"type":3,"invocationId":"1","error":21}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Completion message."], ] as Array<[string, string, string]>).forEach(([name, payload, expectedError]) => - it("throws for " + name, () => { - expect(() => new JsonHubProtocol().parseMessages(payload, NullLogger.instance)) - .toThrow(expectedError); + it("throws for " + name, async () => { + await VerifyLogger.run(async (logger) => { + expect(() => new JsonHubProtocol().parseMessages(payload, logger)) + .toThrow(expectedError); + }); })); - it("can read multiple messages", () => { - const payload = `{"type":2, "invocationId": "abc", "headers": {}, "item": 8}${TextMessageFormat.RecordSeparator}{"type":3, "invocationId": "abc", "headers": {}, "result": "OK"}${TextMessageFormat.RecordSeparator}`; - const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); - expect(messages).toEqual([ - { - headers: {}, - invocationId: "abc", - item: 8, - type: MessageType.StreamItem, - } as StreamItemMessage, - { - headers: {}, - invocationId: "abc", - result: "OK", - type: MessageType.Completion, - } as CompletionMessage, - ]); + it("can read multiple messages", async () => { + await VerifyLogger.run(async (logger) => { + const payload = `{"type":2, "invocationId": "abc", "headers": {}, "item": 8}${TextMessageFormat.RecordSeparator}{"type":3, "invocationId": "abc", "headers": {}, "result": "OK"}${TextMessageFormat.RecordSeparator}`; + const messages = new JsonHubProtocol().parseMessages(payload, logger); + expect(messages).toEqual([ + { + headers: {}, + invocationId: "abc", + item: 8, + type: MessageType.StreamItem, + } as StreamItemMessage, + { + headers: {}, + invocationId: "abc", + result: "OK", + type: MessageType.Completion, + } as CompletionMessage, + ]); + }); }); - it("can read ping message", () => { - const payload = `{"type":6}${TextMessageFormat.RecordSeparator}`; - const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); - expect(messages).toEqual([ - { - type: MessageType.Ping, - }, - ]); + it("can read ping message", async () => { + await VerifyLogger.run(async (logger) => { + const payload = `{"type":6}${TextMessageFormat.RecordSeparator}`; + const messages = new JsonHubProtocol().parseMessages(payload, logger); + expect(messages).toEqual([ + { + type: MessageType.Ping, + }, + ]); + }); }); }); diff --git a/clients/ts/signalr/tests/LongPollingTransport.test.ts b/clients/ts/signalr/tests/LongPollingTransport.test.ts index 66d3715c86..0e25b7502d 100644 --- a/clients/ts/signalr/tests/LongPollingTransport.test.ts +++ b/clients/ts/signalr/tests/LongPollingTransport.test.ts @@ -3,47 +3,56 @@ import { HttpResponse } from "../src/HttpClient"; import { TransferFormat } from "../src/ITransport"; -import { NullLogger } from "../src/Loggers"; import { LongPollingTransport } from "../src/LongPollingTransport"; +import { VerifyLogger } from "./Common"; import { TestHttpClient } from "./TestHttpClient"; import { PromiseSource, SyncPoint } from "./Utils"; describe("LongPollingTransport", () => { it("shuts down polling by aborting in-progress request", async () => { - let firstPoll = true; - const pollCompleted = new PromiseSource(); - const client = new TestHttpClient() - .on("GET", async (r) => { - if (firstPoll) { - firstPoll = false; - return new HttpResponse(200); - } else { - // Turn 'onabort' into a promise. - const abort = new Promise((resolve, reject) => r.abortSignal!.onabort = resolve); - await abort; + await VerifyLogger.run(async (logger) => { + let firstPoll = true; + const pollCompleted = new PromiseSource(); + const client = new TestHttpClient() + .on("GET", async (r) => { + if (firstPoll) { + firstPoll = false; + return new HttpResponse(200); + } else { + // Turn 'onabort' into a promise. + const abort = new Promise((resolve, reject) => { + if (r.abortSignal!.aborted) { + resolve(); + } else { + r.abortSignal!.onabort = resolve; + } + }); + await abort; - // Signal that the poll has completed. - pollCompleted.resolve(); + // Signal that the poll has completed. + pollCompleted.resolve(); - return new HttpResponse(200); - } - }) - .on("DELETE", () => new HttpResponse(202)); - const transport = new LongPollingTransport(client, undefined, NullLogger.instance, false); + return new HttpResponse(200); + } + }) + .on("DELETE", () => new HttpResponse(202)); + const transport = new LongPollingTransport(client, undefined, logger, false); - await transport.connect("http://example.com", TransferFormat.Text); - const stopPromise = transport.stop(); + await transport.connect("http://example.com", TransferFormat.Text); + const stopPromise = transport.stop(); - await pollCompleted.promise; + await pollCompleted.promise; - await stopPromise; + await stopPromise; + }); }); it("204 server response stops polling and raises onClose", async () => { - let firstPoll = true; - const client = new TestHttpClient() - .on("GET", async () => { + await VerifyLogger.run(async (logger) => { + let firstPoll = true; + const client = new TestHttpClient() + .on("GET", async () => { if (firstPoll) { firstPoll = false; return new HttpResponse(200); @@ -52,59 +61,62 @@ describe("LongPollingTransport", () => { return new HttpResponse(204); } }); - const transport = new LongPollingTransport(client, undefined, NullLogger.instance, false); + const transport = new LongPollingTransport(client, undefined, logger, false); - const stopPromise = makeClosedPromise(transport); + const stopPromise = makeClosedPromise(transport); - await transport.connect("http://example.com", TransferFormat.Text); + await transport.connect("http://example.com", TransferFormat.Text); - // Close will be called on transport because of 204 result from polling - await stopPromise; + // Close will be called on transport because of 204 result from polling + await stopPromise; + }); }); it("sends DELETE on stop after polling has finished", async () => { - let firstPoll = true; - let deleteSent = false; - const pollingPromiseSource = new PromiseSource(); - const deleteSyncPoint = new SyncPoint(); - const httpClient = new TestHttpClient() - .on("GET", async (r) => { - if (firstPoll) { - firstPoll = false; - return new HttpResponse(200); - } else { - await pollingPromiseSource.promise; - return new HttpResponse(204); - } - }) - .on("DELETE", async (r) => { - deleteSent = true; - await deleteSyncPoint.waitToContinue(); - return new HttpResponse(202); - }); + await VerifyLogger.run(async (logger) => { + let firstPoll = true; + let deleteSent = false; + const pollingPromiseSource = new PromiseSource(); + const deleteSyncPoint = new SyncPoint(); + const httpClient = new TestHttpClient() + .on("GET", async (r) => { + if (firstPoll) { + firstPoll = false; + return new HttpResponse(200); + } else { + await pollingPromiseSource.promise; + return new HttpResponse(204); + } + }) + .on("DELETE", async (r) => { + deleteSent = true; + await deleteSyncPoint.waitToContinue(); + return new HttpResponse(202); + }); - const transport = new LongPollingTransport(httpClient, undefined, NullLogger.instance, false); + const transport = new LongPollingTransport(httpClient, undefined, logger, false); - await transport.connect("http://tempuri.org", TransferFormat.Text); + await transport.connect("http://tempuri.org", TransferFormat.Text); - // Begin stopping transport - const stopPromise = transport.stop(); + // Begin stopping transport + const stopPromise = transport.stop(); - // Delete will not be sent until polling is finished - expect(deleteSent).toEqual(false); + // Delete will not be sent until polling is finished + expect(deleteSent).toEqual(false); - // Allow polling to complete - pollingPromiseSource.resolve(); + // Allow polling to complete + pollingPromiseSource.resolve(); - // Wait for delete to be called - await deleteSyncPoint.waitForSyncPoint(); + // Wait for delete to be called + await deleteSyncPoint.waitForSyncPoint(); - expect(deleteSent).toEqual(true); + expect(deleteSent).toEqual(true); - deleteSyncPoint.continue(); + deleteSyncPoint.continue(); - // Wait for stop to complete - await stopPromise; + // Wait for stop to complete + await stopPromise; + }); }); });