Add custom header support to Typescript client (#19227)

This commit is contained in:
Sergey Bakulin 2020-03-28 01:08:34 +03:00 committed by GitHub
parent d225883c7d
commit 3fe9012821
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 224 additions and 45 deletions

View File

@ -1123,6 +1123,34 @@ describe("hubConnection", () => {
fail(e);
}
});
it("overwrites library headers with user headers", async (done) => {
const [name] = getUserAgentHeader();
const headers = { [name]: "Custom Agent", "X-HEADER": "VALUE" };
const hubConnection = getConnectionBuilder(t, TESTHUBENDPOINT_URL, { headers })
.withHubProtocol(new JsonHubProtocol())
.build();
try {
await hubConnection.start();
const customUserHeader = await hubConnection.invoke("GetHeader", "X-HEADER");
const headerValue = await hubConnection.invoke("GetHeader", name);
if ((t === HttpTransportType.ServerSentEvents || t === HttpTransportType.WebSockets) && !Platform.isNode) {
expect(headerValue).toBeNull();
expect(customUserHeader).toBeNull();
} else {
expect(headerValue).toEqual("Custom Agent");
expect(customUserHeader).toEqual("VALUE");
}
await hubConnection.stop();
done();
} catch (e) {
fail(e);
}
});
});
function getJwtToken(url: string): Promise<string> {

View File

@ -36,7 +36,7 @@ To use the client in a NodeJS application, install the package to your `node_mod
### Example (Browser)
```JavaScript
```javascript
let connection = new signalR.HubConnectionBuilder()
.withUrl("/chat")
.build();
@ -51,8 +51,7 @@ connection.start()
### Example (WebWorker)
```JavaScript
```javascript
importScripts('signalr.js');
let connection = new signalR.HubConnectionBuilder()
@ -70,7 +69,7 @@ connection.start()
### Example (NodeJS)
```JavaScript
```javascript
const signalR = require("@microsoft/signalr");
let connection = new signalR.HubConnectionBuilder()

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { AbortSignal } from "./AbortController";
import { MessageHeaders } from "./IHubProtocol";
/** Represents an HTTP request. */
export interface HttpRequest {
@ -15,7 +16,7 @@ export interface HttpRequest {
content?: string | ArrayBuffer;
/** An object describing headers to apply to the request. */
headers?: { [key: string]: string };
headers?: MessageHeaders;
/** The XMLHttpRequestResponseType to apply to the request. */
responseType?: XMLHttpRequestResponseType;

View File

@ -315,7 +315,7 @@ export class HttpConnection implements IConnection {
try {
const response = await this.httpClient.post(negotiateUrl, {
content: "",
headers,
headers: { ...headers, ...this.options.headers },
withCredentials: this.options.withCredentials,
});
@ -403,14 +403,14 @@ export class HttpConnection implements IConnection {
if (!this.options.WebSocket) {
throw new Error("'WebSocket' is not supported in your environment.");
}
return new WebSocketTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.WebSocket);
return new WebSocketTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.WebSocket, this.options.headers || {});
case HttpTransportType.ServerSentEvents:
if (!this.options.EventSource) {
throw new Error("'EventSource' is not supported in your environment.");
}
return new ServerSentEventsTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.EventSource, this.options.withCredentials!);
return new ServerSentEventsTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.EventSource, this.options.withCredentials!, this.options.headers || {});
case HttpTransportType.LongPolling:
return new LongPollingTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.withCredentials!);
return new LongPollingTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.withCredentials!, this.options.headers || {});
default:
throw new Error(`Unknown transport: ${transport}.`);
}

View File

@ -2,12 +2,16 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HttpClient } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger";
import { HttpTransportType, ITransport } from "./ITransport";
import { EventSourceConstructor, WebSocketConstructor } from "./Polyfills";
/** Options provided to the 'withUrl' method on {@link @microsoft/signalr.HubConnectionBuilder} to configure options for the HTTP-based transports. */
export interface IHttpConnectionOptions {
/** {@link @microsoft/signalr.MessageHeaders} containing custom headers to be sent with every HTTP request. Note, setting headers in the browser will not work for WebSockets or the ServerSentEvents stream. */
headers?: MessageHeaders;
/** An {@link @microsoft/signalr.HttpClient} that will be used to make HTTP requests. */
httpClient?: HttpClient;

View File

@ -4,6 +4,7 @@
import { AbortController } from "./AbortController";
import { HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport";
import { Arg, getDataDetail, getUserAgentHeader, sendMessage } from "./Utils";
@ -17,6 +18,7 @@ export class LongPollingTransport implements ITransport {
private readonly logMessageContent: boolean;
private readonly withCredentials: boolean;
private readonly pollAbort: AbortController;
private readonly headers: MessageHeaders;
private url?: string;
private running: boolean;
@ -31,13 +33,14 @@ export class LongPollingTransport implements ITransport {
return this.pollAbort.aborted;
}
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, logMessageContent: boolean, withCredentials: boolean) {
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, logMessageContent: boolean, withCredentials: boolean, headers: MessageHeaders) {
this.httpClient = httpClient;
this.accessTokenFactory = accessTokenFactory;
this.logger = logger;
this.pollAbort = new AbortController();
this.logMessageContent = logMessageContent;
this.withCredentials = withCredentials;
this.headers = headers;
this.running = false;
@ -60,9 +63,8 @@ export class LongPollingTransport implements ITransport {
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
}
const headers = {};
const [name, value] = getUserAgentHeader();
headers[name] = value;
const headers = { [name]: value, ...this.headers };
const pollOptions: HttpRequest = {
abortSignal: this.pollAbort.signal,
@ -185,7 +187,7 @@ export class LongPollingTransport implements ITransport {
if (!this.running) {
return Promise.reject(new Error("Cannot send until the transport is connected"));
}
return sendMessage(this.logger, "LongPolling", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent, this.withCredentials);
return sendMessage(this.logger, "LongPolling", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent, this.withCredentials, this.headers);
}
public async stop(): Promise<void> {
@ -206,7 +208,7 @@ export class LongPollingTransport implements ITransport {
headers[name] = value;
const deleteOptions: HttpRequest = {
headers,
headers: { ...headers, ...this.headers },
withCredentials: this.withCredentials,
};
const token = await this.getAccessToken();

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HttpClient } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport";
import { EventSourceConstructor } from "./Polyfills";
@ -17,18 +18,20 @@ export class ServerSentEventsTransport implements ITransport {
private readonly eventSourceConstructor: EventSourceConstructor;
private eventSource?: EventSource;
private url?: string;
private headers: MessageHeaders;
public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null;
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger,
logMessageContent: boolean, eventSourceConstructor: EventSourceConstructor, withCredentials: boolean) {
logMessageContent: boolean, eventSourceConstructor: EventSourceConstructor, withCredentials: boolean, headers: MessageHeaders) {
this.httpClient = httpClient;
this.accessTokenFactory = accessTokenFactory;
this.logger = logger;
this.logMessageContent = logMessageContent;
this.withCredentials = withCredentials;
this.eventSourceConstructor = eventSourceConstructor;
this.headers = headers;
this.onreceive = null;
this.onclose = null;
@ -64,13 +67,12 @@ export class ServerSentEventsTransport implements ITransport {
} else {
// Non-browser passes cookies via the dictionary
const cookies = this.httpClient.getCookieString(url);
const headers = {
Cookie: cookies,
};
const headers: MessageHeaders = {};
headers.Cookie = cookies;
const [name, value] = getUserAgentHeader();
headers[name] = value;
eventSource = new this.eventSourceConstructor(url, { withCredentials: this.withCredentials, headers } as EventSourceInit);
eventSource = new this.eventSourceConstructor(url, { withCredentials: this.withCredentials, headers: { ...headers, ...this.headers} } as EventSourceInit);
}
try {
@ -112,7 +114,7 @@ export class ServerSentEventsTransport implements ITransport {
if (!this.eventSource) {
return Promise.reject(new Error("Cannot send until the transport is connected"));
}
return sendMessage(this.logger, "SSE", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent, this.withCredentials);
return sendMessage(this.logger, "SSE", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent, this.withCredentials, this.headers);
}
public stop(): Promise<void> {

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HttpClient } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger";
import { NullLogger } from "./Loggers";
import { IStreamSubscriber, ISubscription } from "./Stream";
@ -85,7 +86,7 @@ export function isArrayBuffer(val: any): val is ArrayBuffer {
/** @private */
export async function sendMessage(logger: ILogger, transportName: string, httpClient: HttpClient, url: string, accessTokenFactory: (() => string | Promise<string>) | undefined,
content: string | ArrayBuffer, logMessageContent: boolean, withCredentials: boolean): Promise<void> {
content: string | ArrayBuffer, logMessageContent: boolean, withCredentials: boolean, defaultHeaders: MessageHeaders): Promise<void> {
let headers = {};
if (accessTokenFactory) {
const token = await accessTokenFactory();
@ -104,7 +105,7 @@ export async function sendMessage(logger: ILogger, transportName: string, httpCl
const responseType = isArrayBuffer(content) ? "arraybuffer" : "text";
const response = await httpClient.post(url, {
content,
headers,
headers: { ...headers, ...defaultHeaders},
responseType,
withCredentials,
});

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HttpClient } from "./HttpClient";
import { MessageHeaders } from "./IHubProtocol";
import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport";
import { WebSocketConstructor } from "./Polyfills";
@ -15,12 +16,13 @@ export class WebSocketTransport implements ITransport {
private readonly webSocketConstructor: WebSocketConstructor;
private readonly httpClient: HttpClient;
private webSocket?: WebSocket;
private headers: MessageHeaders;
public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null;
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger,
logMessageContent: boolean, webSocketConstructor: WebSocketConstructor) {
logMessageContent: boolean, webSocketConstructor: WebSocketConstructor, headers: MessageHeaders) {
this.logger = logger;
this.accessTokenFactory = accessTokenFactory;
this.logMessageContent = logMessageContent;
@ -29,6 +31,7 @@ export class WebSocketTransport implements ITransport {
this.onreceive = null;
this.onclose = null;
this.headers = headers;
}
public async connect(url: string, transferFormat: TransferFormat): Promise<void> {
@ -59,9 +62,9 @@ export class WebSocketTransport implements ITransport {
headers[`Cookie`] = `${cookies}`;
}
// Only pass cookies when in non-browser environments
// Only pass headers when in non-browser environments
webSocket = new this.webSocketConstructor(url, undefined, {
headers,
headers: { ...headers, ...this.headers },
});
}

View File

@ -44,9 +44,11 @@ describe("HttpClient", () => {
});
await testClient.get("http://localhost", {
headers: { "X-HEADER": "VALUE"},
timeout: 42,
});
expect(request.timeout).toEqual(42);
expect(request.headers).toEqual({ "X-HEADER": "VALUE"});
});
});
@ -86,9 +88,11 @@ describe("HttpClient", () => {
});
await testClient.post("http://localhost", {
headers: { "X-HEADER": "VALUE"},
timeout: 42,
});
expect(request.timeout).toEqual(42);
expect(request.headers).toEqual({ "X-HEADER": "VALUE"});
});
});
});

View File

@ -1152,6 +1152,30 @@ describe("HttpConnection", () => {
}, "Failed to start the connection: Error: nope");
});
it("overwrites library headers with user headers on negotiate", async () => {
await VerifyLogger.run(async (logger) => {
const headers = { "User-Agent": "Custom Agent", "X-HEADER": "VALUE" };
const options: IHttpConnectionOptions = {
...commonOptions,
headers,
httpClient: new TestHttpClient()
.on("POST", (r) => {
expect(r.headers).toEqual(headers);
return new HttpResponse(200, "", "{\"error\":\"nope\"}");
}),
logger,
};
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
} catch {
} finally {
await connection.stop();
}
}, "Failed to start the connection: Error: nope");
});
it("logMessageContent displays correctly with binary data", async () => {
await VerifyLogger.run(async (logger) => {
const availableTransport = { transport: "LongPolling", transferFormats: ["Text", "Binary"] };

View File

@ -24,7 +24,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(200);
} else {
// Turn 'onabort' into a promise.
const abort = new Promise((resolve, reject) => {
const abort = new Promise((resolve) => {
if (r.abortSignal!.aborted) {
resolve();
} else {
@ -40,7 +40,7 @@ describe("LongPollingTransport", () => {
}
})
.on("DELETE", () => new HttpResponse(202));
const transport = new LongPollingTransport(client, undefined, logger, false, true);
const transport = new LongPollingTransport(client, undefined, logger, false, true, {});
await transport.connect("http://example.com", TransferFormat.Text);
const stopPromise = transport.stop();
@ -64,7 +64,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(204);
}
});
const transport = new LongPollingTransport(client, undefined, logger, false, true);
const transport = new LongPollingTransport(client, undefined, logger, false, true, {});
const stopPromise = makeClosedPromise(transport);
@ -82,7 +82,7 @@ describe("LongPollingTransport", () => {
const pollingPromiseSource = new PromiseSource();
const deleteSyncPoint = new SyncPoint();
const httpClient = new TestHttpClient()
.on("GET", async (r) => {
.on("GET", async () => {
if (firstPoll) {
firstPoll = false;
return new HttpResponse(200);
@ -91,13 +91,13 @@ describe("LongPollingTransport", () => {
return new HttpResponse(204);
}
})
.on("DELETE", async (r) => {
.on("DELETE", async () => {
deleteSent = true;
await deleteSyncPoint.waitToContinue();
return new HttpResponse(202);
});
const transport = new LongPollingTransport(httpClient, undefined, logger, false, true);
const transport = new LongPollingTransport(httpClient, undefined, logger, false, true, {});
await transport.connect("http://tempuri.org", TransferFormat.Text);
@ -146,7 +146,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(202);
});
const transport = new LongPollingTransport(httpClient, undefined, logger, false, true);
const transport = new LongPollingTransport(httpClient, undefined, logger, false, true, {});
await transport.connect("http://tempuri.org", TransferFormat.Text);
@ -165,6 +165,67 @@ describe("LongPollingTransport", () => {
expect(secondPollUserAgent).toEqual(value);
});
});
it("overwrites library headers with user headers", async () => {
await VerifyLogger.run(async (logger) => {
const headers = { "User-Agent": "Custom Agent", "X-HEADER": "VALUE" };
let firstPoll = true;
let firstPollUserAgent = "";
let firstUserHeader = "";
let secondPollUserAgent = "";
let secondUserHeader = "";
let deleteUserAgent = "";
let deleteUserHeader = "";
const pollingPromiseSource = new PromiseSource();
const httpClient = new TestHttpClient()
.on("POST", async (r) => {
expect(r.content).toEqual({ message: "hello" });
expect(r.headers).toEqual(headers);
expect(r.method).toEqual("POST");
expect(r.url).toEqual("http://tempuri.org");
})
.on("GET", async (r) => {
if (firstPoll) {
firstPoll = false;
firstPollUserAgent = r.headers!["User-Agent"];
firstUserHeader = r.headers!["X-HEADER"];
return new HttpResponse(200);
} else {
secondPollUserAgent = r.headers!["User-Agent"];
secondUserHeader = r.headers!["X-HEADER"];
await pollingPromiseSource.promise;
return new HttpResponse(204);
}
})
.on("DELETE", async (r) => {
deleteUserAgent = r.headers!["User-Agent"];
deleteUserHeader = r.headers!["X-HEADER"];
return new HttpResponse(202);
});
const transport = new LongPollingTransport(httpClient, undefined, logger, false, true, headers);
await transport.connect("http://tempuri.org", TransferFormat.Text);
await transport.send({ message: "hello" });
// Begin stopping transport
const stopPromise = transport.stop();
// Allow polling to complete
pollingPromiseSource.resolve();
// Wait for stop to complete
await stopPromise;
expect(firstPollUserAgent).toEqual("Custom Agent");
expect(deleteUserAgent).toEqual("Custom Agent");
expect(secondPollUserAgent).toEqual("Custom Agent");
expect(firstUserHeader).toEqual("VALUE");
expect(secondUserHeader).toEqual("VALUE");
expect(deleteUserHeader).toEqual("VALUE");
});
});
});
function makeClosedPromise(transport: LongPollingTransport): Promise<void> {

View File

@ -1,6 +1,7 @@
// 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 { MessageHeaders } from "../src/IHubProtocol";
import { TransferFormat } from "../src/ITransport";
import { HttpClient, HttpRequest } from "../src/HttpClient";
@ -17,7 +18,7 @@ registerUnhandledRejectionHandler();
describe("ServerSentEventsTransport", () => {
it("does not allow non-text formats", async () => {
await VerifyLogger.run(async (logger) => {
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true);
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true, {});
await expect(sse.connect("", TransferFormat.Binary))
.rejects
@ -27,7 +28,7 @@ describe("ServerSentEventsTransport", () => {
it("connect waits for EventSource to be connected", async () => {
await VerifyLogger.run(async (logger) => {
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true);
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true, {});
let connectComplete: boolean = false;
const connectPromise = (async () => {
@ -48,7 +49,7 @@ describe("ServerSentEventsTransport", () => {
it("connect failure does not call onclose handler", async () => {
await VerifyLogger.run(async (logger) => {
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true);
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true, {});
let closeCalled = false;
sse.onclose = () => closeCalled = true;
@ -169,7 +170,7 @@ describe("ServerSentEventsTransport", () => {
it("send throws if not connected", async () => {
await VerifyLogger.run(async (logger) => {
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true);
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource, true, {});
await expect(sse.send(""))
.rejects
@ -221,10 +222,31 @@ describe("ServerSentEventsTransport", () => {
expect(request!.url).toBe("http://example.com");
});
});
it("overwrites library headers with user headers", async () => {
await VerifyLogger.run(async (logger) => {
let request: HttpRequest;
const httpClient = new TestHttpClient().on((r) => {
request = r;
return "";
});
const headers = { "User-Agent": "Custom Agent", "X-HEADER": "VALUE" };
const sse = await createAndStartSSE(logger, "http://example.com", undefined, httpClient, headers);
expect((TestEventSource.eventSource.eventSourceInitDict as any).headers["User-Agent"]).toEqual("Custom Agent");
expect((TestEventSource.eventSource.eventSourceInitDict as any).headers["X-HEADER"]).toEqual("VALUE");
await sse.send("");
expect(request!.headers!["User-Agent"]).toEqual("Custom Agent");
expect(request!.headers!["X-HEADER"]).toEqual("VALUE");
expect(request!.url).toBe("http://example.com");
});
});
});
async function createAndStartSSE(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), httpClient?: HttpClient): Promise<ServerSentEventsTransport> {
const sse = new ServerSentEventsTransport(httpClient || new TestHttpClient(), accessTokenFactory, logger, true, TestEventSource, true);
async function createAndStartSSE(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), httpClient?: HttpClient, headers?: MessageHeaders): Promise<ServerSentEventsTransport> {
const sse = new ServerSentEventsTransport(httpClient || new TestHttpClient(), accessTokenFactory, logger, true, TestEventSource, true, headers || {});
const connectPromise = sse.connect(url || "http://example.com", TransferFormat.Text);
await TestEventSource.eventSource.openSet;

View File

@ -1,6 +1,7 @@
// 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 { MessageHeaders } from "../src/IHubProtocol";
import { ILogger } from "../src/ILogger";
import { TransferFormat } from "../src/ITransport";
import { getUserAgentHeader } from "../src/Utils";
@ -24,7 +25,7 @@ describe("WebSocketTransport", () => {
it("connect waits for WebSocket to be connected", async () => {
await VerifyLogger.run(async (logger) => {
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket);
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket, {});
let connectComplete: boolean = false;
const connectPromise = (async () => {
@ -46,7 +47,7 @@ describe("WebSocketTransport", () => {
it("connect fails if there is error during connect", async () => {
await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestErrorEvent;
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket);
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket, {});
let connectComplete: boolean = false;
const connectPromise = (async () => {
@ -70,7 +71,7 @@ describe("WebSocketTransport", () => {
it("connect failure does not call onclose handler", async () => {
await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestErrorEvent;
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket);
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket, {});
let closeCalled = false;
webSocket.onclose = () => closeCalled = true;
@ -257,6 +258,33 @@ describe("WebSocketTransport", () => {
});
});
it("overwrites library headers with user headers", async () => {
await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestEvent;
const headers = { "User-Agent": "Custom Agent", "X-HEADER": "VALUE" };
const webSocket = await createAndStartWebSocket(logger, undefined, undefined, undefined, headers);
let closeCalled: boolean = false;
let error: Error;
webSocket.onclose = (e) => {
closeCalled = true;
error = e!;
};
expect(TestWebSocket.webSocket.options!.headers[`User-Agent`]).toEqual("Custom Agent");
expect(TestWebSocket.webSocket.options!.headers[`X-HEADER`]).toEqual("VALUE");
await webSocket.stop();
expect(closeCalled).toBe(true);
expect(error!).toBeUndefined();
await expect(webSocket.send(""))
.rejects
.toBe("WebSocket is not in the OPEN state");
});
});
it("is closed from 'onreceive' callback throwing", async () => {
await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestEvent;
@ -270,7 +298,7 @@ describe("WebSocketTransport", () => {
};
const receiveError = new Error("callback error");
webSocket.onreceive = (data) => {
webSocket.onreceive = () => {
throw receiveError;
};
@ -290,7 +318,7 @@ describe("WebSocketTransport", () => {
it("does not run onclose callback if Transport does not fully connect and exits", async () => {
await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestErrorEvent;
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket);
const webSocket = new WebSocketTransport(new TestHttpClient(), undefined, logger, true, TestWebSocket, {});
const connectPromise = webSocket.connect("http://example.com", TransferFormat.Text);
@ -318,8 +346,8 @@ describe("WebSocketTransport", () => {
});
});
async function createAndStartWebSocket(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), format?: TransferFormat): Promise<WebSocketTransport> {
const webSocket = new WebSocketTransport(new TestHttpClient(), accessTokenFactory, logger, true, TestWebSocket);
async function createAndStartWebSocket(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), format?: TransferFormat, headers?: MessageHeaders): Promise<WebSocketTransport> {
const webSocket = new WebSocketTransport(new TestHttpClient(), accessTokenFactory, logger, true, TestWebSocket, headers || {});
const connectPromise = webSocket.connect(url || "http://example.com", format || TransferFormat.Text);