[TS] Add support for negotiateVersion and connectionToken (#14157)
This commit is contained in:
parent
f3e6b74623
commit
a2d217c444
|
|
@ -23,6 +23,8 @@ const enum ConnectionState {
|
||||||
/** @private */
|
/** @private */
|
||||||
export interface INegotiateResponse {
|
export interface INegotiateResponse {
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
|
connectionToken?: string;
|
||||||
|
negotiateVersion?: number;
|
||||||
availableTransports?: IAvailableTransport[];
|
availableTransports?: IAvailableTransport[];
|
||||||
url?: string;
|
url?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
@ -70,6 +72,8 @@ export class HttpConnection implements IConnection {
|
||||||
public onreceive: ((data: string | ArrayBuffer) => void) | null;
|
public onreceive: ((data: string | ArrayBuffer) => void) | null;
|
||||||
public onclose: ((e?: Error) => void) | null;
|
public onclose: ((e?: Error) => void) | null;
|
||||||
|
|
||||||
|
private readonly negotiateVersion: number = 1;
|
||||||
|
|
||||||
constructor(url: string, options: IHttpConnectionOptions = {}) {
|
constructor(url: string, options: IHttpConnectionOptions = {}) {
|
||||||
Arg.isRequired(url, "url");
|
Arg.isRequired(url, "url");
|
||||||
|
|
||||||
|
|
@ -272,8 +276,6 @@ export class HttpConnection implements IConnection {
|
||||||
throw new Error("Negotiate redirection limit exceeded.");
|
throw new Error("Negotiate redirection limit exceeded.");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connectionId = negotiateResponse.connectionId;
|
|
||||||
|
|
||||||
await this.createTransport(url, this.options.transport, negotiateResponse, transferFormat);
|
await this.createTransport(url, this.options.transport, negotiateResponse, transferFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,32 +324,41 @@ export class HttpConnection implements IConnection {
|
||||||
return Promise.reject(new Error(`Unexpected status code returned from negotiate ${response.statusCode}`));
|
return Promise.reject(new Error(`Unexpected status code returned from negotiate ${response.statusCode}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(response.content as string) as INegotiateResponse;
|
const negotiateResponse = JSON.parse(response.content as string) as INegotiateResponse;
|
||||||
|
if (!negotiateResponse.negotiateVersion || negotiateResponse.negotiateVersion < 1) {
|
||||||
|
// Negotiate version 0 doesn't use connectionToken
|
||||||
|
// So we set it equal to connectionId so all our logic can use connectionToken without being aware of the negotiate version
|
||||||
|
negotiateResponse.connectionToken = negotiateResponse.connectionId;
|
||||||
|
}
|
||||||
|
return negotiateResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.log(LogLevel.Error, "Failed to complete negotiation with the server: " + e);
|
this.logger.log(LogLevel.Error, "Failed to complete negotiation with the server: " + e);
|
||||||
return Promise.reject(e);
|
return Promise.reject(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createConnectUrl(url: string, connectionId: string | null | undefined) {
|
private createConnectUrl(url: string, connectionToken: string | null | undefined) {
|
||||||
if (!connectionId) {
|
if (!connectionToken) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
return url + (url.indexOf("?") === -1 ? "?" : "&") + `id=${connectionId}`;
|
|
||||||
|
return url + (url.indexOf("?") === -1 ? "?" : "&") + `id=${connectionToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createTransport(url: string, requestedTransport: HttpTransportType | ITransport | undefined, negotiateResponse: INegotiateResponse, requestedTransferFormat: TransferFormat): Promise<void> {
|
private async createTransport(url: string, requestedTransport: HttpTransportType | ITransport | undefined, negotiateResponse: INegotiateResponse, requestedTransferFormat: TransferFormat): Promise<void> {
|
||||||
let connectUrl = this.createConnectUrl(url, negotiateResponse.connectionId);
|
let connectUrl = this.createConnectUrl(url, negotiateResponse.connectionToken);
|
||||||
if (this.isITransport(requestedTransport)) {
|
if (this.isITransport(requestedTransport)) {
|
||||||
this.logger.log(LogLevel.Debug, "Connection was provided an instance of ITransport, using that directly.");
|
this.logger.log(LogLevel.Debug, "Connection was provided an instance of ITransport, using that directly.");
|
||||||
this.transport = requestedTransport;
|
this.transport = requestedTransport;
|
||||||
await this.startTransport(connectUrl, requestedTransferFormat);
|
await this.startTransport(connectUrl, requestedTransferFormat);
|
||||||
|
|
||||||
|
this.connectionId = negotiateResponse.connectionId;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transportExceptions: any[] = [];
|
const transportExceptions: any[] = [];
|
||||||
const transports = negotiateResponse.availableTransports || [];
|
const transports = negotiateResponse.availableTransports || [];
|
||||||
|
let negotiate: INegotiateResponse | undefined = negotiateResponse;
|
||||||
for (const endpoint of transports) {
|
for (const endpoint of transports) {
|
||||||
const transportOrError = this.resolveTransportOrError(endpoint, requestedTransport, requestedTransferFormat);
|
const transportOrError = this.resolveTransportOrError(endpoint, requestedTransport, requestedTransferFormat);
|
||||||
if (transportOrError instanceof Error) {
|
if (transportOrError instanceof Error) {
|
||||||
|
|
@ -355,20 +366,21 @@ export class HttpConnection implements IConnection {
|
||||||
transportExceptions.push(`${endpoint.transport} failed: ${transportOrError}`);
|
transportExceptions.push(`${endpoint.transport} failed: ${transportOrError}`);
|
||||||
} else if (this.isITransport(transportOrError)) {
|
} else if (this.isITransport(transportOrError)) {
|
||||||
this.transport = transportOrError;
|
this.transport = transportOrError;
|
||||||
if (!negotiateResponse.connectionId) {
|
if (!negotiate) {
|
||||||
try {
|
try {
|
||||||
negotiateResponse = await this.getNegotiationResponse(url);
|
negotiate = await this.getNegotiationResponse(url);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
return Promise.reject(ex);
|
return Promise.reject(ex);
|
||||||
}
|
}
|
||||||
connectUrl = this.createConnectUrl(url, negotiateResponse.connectionId);
|
connectUrl = this.createConnectUrl(url, negotiate.connectionToken);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.startTransport(connectUrl, requestedTransferFormat);
|
await this.startTransport(connectUrl, requestedTransferFormat);
|
||||||
|
this.connectionId = negotiate.connectionId;
|
||||||
return;
|
return;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
this.logger.log(LogLevel.Error, `Failed to start the transport '${endpoint.transport}': ${ex}`);
|
this.logger.log(LogLevel.Error, `Failed to start the transport '${endpoint.transport}': ${ex}`);
|
||||||
negotiateResponse.connectionId = undefined;
|
negotiate = undefined;
|
||||||
transportExceptions.push(`${endpoint.transport} failed: ${ex}`);
|
transportExceptions.push(`${endpoint.transport} failed: ${ex}`);
|
||||||
|
|
||||||
if (this.connectionState !== ConnectionState.Connecting) {
|
if (this.connectionState !== ConnectionState.Connecting) {
|
||||||
|
|
@ -504,7 +516,7 @@ export class HttpConnection implements IConnection {
|
||||||
|
|
||||||
// Setting the url to the href propery of an anchor tag handles normalization
|
// Setting the url to the href propery of an anchor tag handles normalization
|
||||||
// for us. There are 3 main cases.
|
// for us. There are 3 main cases.
|
||||||
// 1. Relative path normalization e.g "b" -> "http://localhost:5000/a/b"
|
// 1. Relative path normalization e.g "b" -> "http://localhost:5000/a/b"
|
||||||
// 2. Absolute path normalization e.g "/a/b" -> "http://localhost:5000/a/b"
|
// 2. Absolute path normalization e.g "/a/b" -> "http://localhost:5000/a/b"
|
||||||
// 3. Networkpath reference normalization e.g "//localhost:5000/a/b" -> "http://localhost:5000/a/b"
|
// 3. Networkpath reference normalization e.g "//localhost:5000/a/b" -> "http://localhost:5000/a/b"
|
||||||
const aTag = window.document.createElement("a");
|
const aTag = window.document.createElement("a");
|
||||||
|
|
@ -522,6 +534,11 @@ export class HttpConnection implements IConnection {
|
||||||
}
|
}
|
||||||
negotiateUrl += "negotiate";
|
negotiateUrl += "negotiate";
|
||||||
negotiateUrl += index === -1 ? "" : url.substring(index);
|
negotiateUrl += index === -1 ? "" : url.substring(index);
|
||||||
|
|
||||||
|
if (negotiateUrl.indexOf("negotiateVersion") === -1) {
|
||||||
|
negotiateUrl += index === -1 ? "?" : "&";
|
||||||
|
negotiateUrl += "negotiateVersion=" + this.negotiateVersion;
|
||||||
|
}
|
||||||
return negotiateUrl;
|
return negotiateUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@ export function eachTransport(action: (transport: HttpTransportType) => void) {
|
||||||
|
|
||||||
export function eachEndpointUrl(action: (givenUrl: string, expectedUrl: string) => void) {
|
export function eachEndpointUrl(action: (givenUrl: string, expectedUrl: string) => void) {
|
||||||
const urls = [
|
const urls = [
|
||||||
[ "http://tempuri.org/endpoint/?q=my/Data", "http://tempuri.org/endpoint/negotiate?q=my/Data" ],
|
[ "http://tempuri.org/endpoint/?q=my/Data", "http://tempuri.org/endpoint/negotiate?q=my/Data&negotiateVersion=1" ],
|
||||||
[ "http://tempuri.org/endpoint?q=my/Data", "http://tempuri.org/endpoint/negotiate?q=my/Data" ],
|
[ "http://tempuri.org/endpoint?q=my/Data", "http://tempuri.org/endpoint/negotiate?q=my/Data&negotiateVersion=1" ],
|
||||||
[ "http://tempuri.org/endpoint", "http://tempuri.org/endpoint/negotiate" ],
|
[ "http://tempuri.org/endpoint", "http://tempuri.org/endpoint/negotiate?negotiateVersion=1" ],
|
||||||
[ "http://tempuri.org/endpoint/", "http://tempuri.org/endpoint/negotiate" ],
|
[ "http://tempuri.org/endpoint/", "http://tempuri.org/endpoint/negotiate?negotiateVersion=1" ],
|
||||||
];
|
];
|
||||||
|
|
||||||
urls.forEach((t) => action(t[0], t[1]));
|
urls.forEach((t) => action(t[0], t[1]));
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { EventSourceConstructor, WebSocketConstructor } from "../src/Polyfills";
|
||||||
import { eachEndpointUrl, eachTransport, VerifyLogger } from "./Common";
|
import { eachEndpointUrl, eachTransport, VerifyLogger } from "./Common";
|
||||||
import { TestHttpClient } from "./TestHttpClient";
|
import { TestHttpClient } from "./TestHttpClient";
|
||||||
import { TestTransport } from "./TestTransport";
|
import { TestTransport } from "./TestTransport";
|
||||||
|
import { TestEvent, TestWebSocket } from "./TestWebSocket";
|
||||||
import { PromiseSource, registerUnhandledRejectionHandler, SyncPoint } from "./Utils";
|
import { PromiseSource, registerUnhandledRejectionHandler, SyncPoint } from "./Utils";
|
||||||
|
|
||||||
const commonOptions: IHttpConnectionOptions = {
|
const commonOptions: IHttpConnectionOptions = {
|
||||||
|
|
@ -20,6 +21,7 @@ const commonOptions: IHttpConnectionOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultConnectionId = "abc123";
|
const defaultConnectionId = "abc123";
|
||||||
|
const defaultConnectionToken = "123abc";
|
||||||
const defaultNegotiateResponse: INegotiateResponse = {
|
const defaultNegotiateResponse: INegotiateResponse = {
|
||||||
availableTransports: [
|
availableTransports: [
|
||||||
{ transport: "WebSockets", transferFormats: ["Text", "Binary"] },
|
{ transport: "WebSockets", transferFormats: ["Text", "Binary"] },
|
||||||
|
|
@ -27,6 +29,8 @@ const defaultNegotiateResponse: INegotiateResponse = {
|
||||||
{ transport: "LongPolling", transferFormats: ["Text", "Binary"] },
|
{ transport: "LongPolling", transferFormats: ["Text", "Binary"] },
|
||||||
],
|
],
|
||||||
connectionId: defaultConnectionId,
|
connectionId: defaultConnectionId,
|
||||||
|
connectionToken: defaultConnectionToken,
|
||||||
|
negotiateVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
registerUnhandledRejectionHandler();
|
registerUnhandledRejectionHandler();
|
||||||
|
|
@ -571,7 +575,7 @@ describe("HttpConnection", () => {
|
||||||
let firstNegotiate = true;
|
let firstNegotiate = true;
|
||||||
let firstPoll = true;
|
let firstPoll = true;
|
||||||
const httpClient = new TestHttpClient()
|
const httpClient = new TestHttpClient()
|
||||||
.on("POST", /negotiate$/, () => {
|
.on("POST", /\/negotiate/, () => {
|
||||||
if (firstNegotiate) {
|
if (firstNegotiate) {
|
||||||
firstNegotiate = false;
|
firstNegotiate = false;
|
||||||
return { url: "https://another.domain.url/chat" };
|
return { url: "https://another.domain.url/chat" };
|
||||||
|
|
@ -602,8 +606,8 @@ describe("HttpConnection", () => {
|
||||||
await connection.start(TransferFormat.Text);
|
await connection.start(TransferFormat.Text);
|
||||||
|
|
||||||
expect(httpClient.sentRequests.length).toBe(4);
|
expect(httpClient.sentRequests.length).toBe(4);
|
||||||
expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate");
|
expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate?negotiateVersion=1");
|
||||||
expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate");
|
expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate?negotiateVersion=1");
|
||||||
expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
|
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);
|
expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -615,7 +619,7 @@ describe("HttpConnection", () => {
|
||||||
it("fails to start if negotiate redirects more than 100 times", async () => {
|
it("fails to start if negotiate redirects more than 100 times", async () => {
|
||||||
await VerifyLogger.run(async (logger) => {
|
await VerifyLogger.run(async (logger) => {
|
||||||
const httpClient = new TestHttpClient()
|
const httpClient = new TestHttpClient()
|
||||||
.on("POST", /negotiate$/, () => ({ url: "https://another.domain.url/chat" }));
|
.on("POST", /\/negotiate/, () => ({ url: "https://another.domain.url/chat" }));
|
||||||
|
|
||||||
const options: IHttpConnectionOptions = {
|
const options: IHttpConnectionOptions = {
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
|
|
@ -637,7 +641,7 @@ describe("HttpConnection", () => {
|
||||||
let firstNegotiate = true;
|
let firstNegotiate = true;
|
||||||
let firstPoll = true;
|
let firstPoll = true;
|
||||||
const httpClient = new TestHttpClient()
|
const httpClient = new TestHttpClient()
|
||||||
.on("POST", /negotiate$/, (r) => {
|
.on("POST", /\/negotiate/, (r) => {
|
||||||
if (firstNegotiate) {
|
if (firstNegotiate) {
|
||||||
firstNegotiate = false;
|
firstNegotiate = false;
|
||||||
|
|
||||||
|
|
@ -683,8 +687,8 @@ describe("HttpConnection", () => {
|
||||||
await connection.start(TransferFormat.Text);
|
await connection.start(TransferFormat.Text);
|
||||||
|
|
||||||
expect(httpClient.sentRequests.length).toBe(4);
|
expect(httpClient.sentRequests.length).toBe(4);
|
||||||
expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate");
|
expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate?negotiateVersion=1");
|
||||||
expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate");
|
expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate?negotiateVersion=1");
|
||||||
expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
|
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);
|
expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -696,7 +700,7 @@ describe("HttpConnection", () => {
|
||||||
it("throws error if negotiate response has error", async () => {
|
it("throws error if negotiate response has error", async () => {
|
||||||
await VerifyLogger.run(async (logger) => {
|
await VerifyLogger.run(async (logger) => {
|
||||||
const httpClient = new TestHttpClient()
|
const httpClient = new TestHttpClient()
|
||||||
.on("POST", /negotiate$/, () => ({ error: "Negotiate error." }));
|
.on("POST", /\/negotiate/, () => ({ error: "Negotiate error." }));
|
||||||
|
|
||||||
const options: IHttpConnectionOptions = {
|
const options: IHttpConnectionOptions = {
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
|
|
@ -873,6 +877,253 @@ describe("HttpConnection", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("missing negotiateVersion ignores connectionToken", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const availableTransport = { transport: "Custom", transferFormats: ["Text"] };
|
||||||
|
const transport = {
|
||||||
|
connect(url: string, transferFormat: TransferFormat) {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
send(data: any) {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
if (transport.onclose) {
|
||||||
|
transport.onclose();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
onclose: null,
|
||||||
|
onreceive: null,
|
||||||
|
} as ITransport;
|
||||||
|
const options: IHttpConnectionOptions = {
|
||||||
|
...commonOptions,
|
||||||
|
httpClient: new TestHttpClient()
|
||||||
|
.on("POST", () => ({ connectionId: "42", connectionToken: "token", availableTransports: [availableTransport] })),
|
||||||
|
logger,
|
||||||
|
transport,
|
||||||
|
} as IHttpConnectionOptions;
|
||||||
|
|
||||||
|
const connection = new HttpConnection("http://tempuri.org", options);
|
||||||
|
connection.onreceive = () => null;
|
||||||
|
try {
|
||||||
|
await connection.start(TransferFormat.Text);
|
||||||
|
expect(connection.connectionId).toBe("42");
|
||||||
|
} finally {
|
||||||
|
await connection.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("negotiate version 0 ignores connectionToken", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const availableTransport = { transport: "Custom", transferFormats: ["Text"] };
|
||||||
|
const transport = {
|
||||||
|
connect(url: string, transferFormat: TransferFormat) {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
send(data: any) {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
if (transport.onclose) {
|
||||||
|
transport.onclose();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
onclose: null,
|
||||||
|
onreceive: null,
|
||||||
|
} as ITransport;
|
||||||
|
const options: IHttpConnectionOptions = {
|
||||||
|
...commonOptions,
|
||||||
|
httpClient: new TestHttpClient()
|
||||||
|
.on("POST", () => ({ connectionId: "42", connectionToken: "token", negotiateVersion: 0, availableTransports: [availableTransport] })),
|
||||||
|
logger,
|
||||||
|
transport,
|
||||||
|
} as IHttpConnectionOptions;
|
||||||
|
|
||||||
|
const connection = new HttpConnection("http://tempuri.org", options);
|
||||||
|
connection.onreceive = () => null;
|
||||||
|
try {
|
||||||
|
await connection.start(TransferFormat.Text);
|
||||||
|
expect(connection.connectionId).toBe("42");
|
||||||
|
} finally {
|
||||||
|
await connection.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("negotiate version 1 uses connectionToken for url and connectionId for property", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const availableTransport = { transport: "Custom", transferFormats: ["Text"] };
|
||||||
|
let connectUrl = "";
|
||||||
|
const transport = {
|
||||||
|
connect(url: string, transferFormat: TransferFormat) {
|
||||||
|
connectUrl = url;
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
send(data: any) {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
if (transport.onclose) {
|
||||||
|
transport.onclose();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
onclose: null,
|
||||||
|
onreceive: null,
|
||||||
|
} as ITransport;
|
||||||
|
const options: IHttpConnectionOptions = {
|
||||||
|
...commonOptions,
|
||||||
|
httpClient: new TestHttpClient()
|
||||||
|
.on("POST", () => ({ connectionId: "42", connectionToken: "token", negotiateVersion: 1, availableTransports: [availableTransport] })),
|
||||||
|
logger,
|
||||||
|
transport,
|
||||||
|
} as IHttpConnectionOptions;
|
||||||
|
|
||||||
|
const connection = new HttpConnection("http://tempuri.org", options);
|
||||||
|
connection.onreceive = () => null;
|
||||||
|
try {
|
||||||
|
await connection.start(TransferFormat.Text);
|
||||||
|
expect(connection.connectionId).toBe("42");
|
||||||
|
expect(connectUrl).toBe("http://tempuri.org?id=token");
|
||||||
|
} finally {
|
||||||
|
await connection.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("negotiateVersion query string not added if already present", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const connectUrl = new PromiseSource<string>();
|
||||||
|
const fakeTransport: ITransport = {
|
||||||
|
connect(url: string): Promise<void> {
|
||||||
|
connectUrl.resolve(url);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
send(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
stop(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
onclose: null,
|
||||||
|
onreceive: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: IHttpConnectionOptions = {
|
||||||
|
...commonOptions,
|
||||||
|
httpClient: new TestHttpClient()
|
||||||
|
.on("POST", "http://tempuri.org/negotiate?negotiateVersion=42", () => "{ \"connectionId\": \"42\" }")
|
||||||
|
.on("GET", () => ""),
|
||||||
|
logger,
|
||||||
|
transport: fakeTransport,
|
||||||
|
} as IHttpConnectionOptions;
|
||||||
|
|
||||||
|
const connection = new HttpConnection("http://tempuri.org?negotiateVersion=42", options);
|
||||||
|
try {
|
||||||
|
const startPromise = connection.start(TransferFormat.Text);
|
||||||
|
|
||||||
|
expect(await connectUrl).toBe("http://tempuri.org?negotiateVersion=42&id=42");
|
||||||
|
|
||||||
|
await startPromise;
|
||||||
|
} finally {
|
||||||
|
(options.transport as ITransport).onclose!();
|
||||||
|
await connection.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("negotiateVersion query string not added if already present after redirect", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const connectUrl = new PromiseSource<string>();
|
||||||
|
const fakeTransport: ITransport = {
|
||||||
|
connect(url: string): Promise<void> {
|
||||||
|
connectUrl.resolve(url);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
send(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
stop(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
onclose: null,
|
||||||
|
onreceive: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: IHttpConnectionOptions = {
|
||||||
|
...commonOptions,
|
||||||
|
httpClient: new TestHttpClient()
|
||||||
|
.on("POST", "http://tempuri.org/negotiate?negotiateVersion=1", () => "{ \"url\": \"http://redirect.org\" }")
|
||||||
|
.on("POST", "http://redirect.org/negotiate?negotiateVersion=1", () => "{ \"connectionId\": \"42\"}")
|
||||||
|
.on("GET", () => ""),
|
||||||
|
logger,
|
||||||
|
transport: fakeTransport,
|
||||||
|
} as IHttpConnectionOptions;
|
||||||
|
|
||||||
|
const connection = new HttpConnection("http://tempuri.org", options);
|
||||||
|
try {
|
||||||
|
const startPromise = connection.start(TransferFormat.Text);
|
||||||
|
|
||||||
|
expect(await connectUrl).toBe("http://redirect.org?id=42");
|
||||||
|
|
||||||
|
await startPromise;
|
||||||
|
} finally {
|
||||||
|
(options.transport as ITransport).onclose!();
|
||||||
|
await connection.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fallback changes connectionId property", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const availableTransports = [{ transport: "WebSockets", transferFormats: ["Text"] }, { transport: "LongPolling", transferFormats: ["Text"] }];
|
||||||
|
let negotiateCount: number = 0;
|
||||||
|
let getCount: number = 0;
|
||||||
|
let connection: HttpConnection;
|
||||||
|
let connectionId: string | undefined;
|
||||||
|
const options: IHttpConnectionOptions = {
|
||||||
|
WebSocket: TestWebSocket,
|
||||||
|
...commonOptions,
|
||||||
|
httpClient: new TestHttpClient()
|
||||||
|
.on("POST", () => {
|
||||||
|
negotiateCount++;
|
||||||
|
return ({ connectionId: negotiateCount.toString(), connectionToken: "token", negotiateVersion: 1, availableTransports });
|
||||||
|
})
|
||||||
|
.on("GET", () => {
|
||||||
|
getCount++;
|
||||||
|
if (getCount === 1) {
|
||||||
|
return new HttpResponse(200);
|
||||||
|
}
|
||||||
|
connectionId = connection.connectionId;
|
||||||
|
return new HttpResponse(204);
|
||||||
|
})
|
||||||
|
.on("DELETE", () => new HttpResponse(202)),
|
||||||
|
|
||||||
|
logger,
|
||||||
|
} as IHttpConnectionOptions;
|
||||||
|
|
||||||
|
TestWebSocket.webSocketSet = new PromiseSource();
|
||||||
|
|
||||||
|
connection = new HttpConnection("http://tempuri.org", options);
|
||||||
|
const startPromise = connection.start(TransferFormat.Text);
|
||||||
|
|
||||||
|
await TestWebSocket.webSocketSet;
|
||||||
|
await TestWebSocket.webSocket.closeSet;
|
||||||
|
TestWebSocket.webSocket.onerror(new TestEvent());
|
||||||
|
|
||||||
|
try {
|
||||||
|
await startPromise;
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
expect(negotiateCount).toEqual(2);
|
||||||
|
expect(connectionId).toEqual("2");
|
||||||
|
},
|
||||||
|
"Failed to start the transport 'WebSockets': Error: There was an error with the transport.");
|
||||||
|
});
|
||||||
|
|
||||||
describe(".constructor", () => {
|
describe(".constructor", () => {
|
||||||
it("throws if no Url is provided", async () => {
|
it("throws if no Url is provided", async () => {
|
||||||
// Force TypeScript to let us call the constructor incorrectly :)
|
// Force TypeScript to let us call the constructor incorrectly :)
|
||||||
|
|
@ -921,7 +1172,7 @@ describe("HttpConnection", () => {
|
||||||
|
|
||||||
it("uses WebSocket constructor from options if provided", async () => {
|
it("uses WebSocket constructor from options if provided", async () => {
|
||||||
await VerifyLogger.run(async (logger) => {
|
await VerifyLogger.run(async (logger) => {
|
||||||
class TestWebSocket {
|
class BadConstructorWebSocket {
|
||||||
// The "_" prefix tell TypeScript not to worry about unused parameter, but tslint doesn't like it.
|
// The "_" prefix tell TypeScript not to worry about unused parameter, but tslint doesn't like it.
|
||||||
// tslint:disable-next-line:variable-name
|
// tslint:disable-next-line:variable-name
|
||||||
constructor(_url: string, _protocols?: string | string[]) {
|
constructor(_url: string, _protocols?: string | string[]) {
|
||||||
|
|
@ -931,7 +1182,7 @@ describe("HttpConnection", () => {
|
||||||
|
|
||||||
const options: IHttpConnectionOptions = {
|
const options: IHttpConnectionOptions = {
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
WebSocket: TestWebSocket as WebSocketConstructor,
|
WebSocket: BadConstructorWebSocket as WebSocketConstructor,
|
||||||
logger,
|
logger,
|
||||||
skipNegotiation: true,
|
skipNegotiation: true,
|
||||||
transport: HttpTransportType.WebSockets,
|
transport: HttpTransportType.WebSockets,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ const longPollingNegotiateResponse = {
|
||||||
{ transport: "LongPolling", transferFormats: ["Text", "Binary"] },
|
{ transport: "LongPolling", transferFormats: ["Text", "Binary"] },
|
||||||
],
|
],
|
||||||
connectionId: "abc123",
|
connectionId: "abc123",
|
||||||
|
connectionToken: "123abc",
|
||||||
|
negotiateVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const commonHttpOptions: IHttpConnectionOptions = {
|
const commonHttpOptions: IHttpConnectionOptions = {
|
||||||
|
|
@ -88,7 +90,7 @@ describe("HubConnectionBuilder", () => {
|
||||||
const pollSent = new PromiseSource<HttpRequest>();
|
const pollSent = new PromiseSource<HttpRequest>();
|
||||||
const pollCompleted = new PromiseSource<HttpResponse>();
|
const pollCompleted = new PromiseSource<HttpResponse>();
|
||||||
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
||||||
.on("POST", "http://example.com?id=abc123", (req) => {
|
.on("POST", "http://example.com?id=123abc", (req) => {
|
||||||
// Respond from the poll with the handshake response
|
// Respond from the poll with the handshake response
|
||||||
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
||||||
return new HttpResponse(202);
|
return new HttpResponse(202);
|
||||||
|
|
@ -104,7 +106,7 @@ describe("HubConnectionBuilder", () => {
|
||||||
await expect(connection.start()).rejects.toThrow("The underlying connection was closed before the hub handshake could complete.");
|
await expect(connection.start()).rejects.toThrow("The underlying connection was closed before the hub handshake could complete.");
|
||||||
expect(connection.state).toBe(HubConnectionState.Disconnected);
|
expect(connection.state).toBe(HubConnectionState.Disconnected);
|
||||||
|
|
||||||
expect((await pollSent.promise).url).toMatch(/http:\/\/example.com\?id=abc123.*/);
|
expect((await pollSent.promise).url).toMatch(/http:\/\/example.com\?id=123abc.*/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -125,7 +127,7 @@ describe("HubConnectionBuilder", () => {
|
||||||
const pollCompleted = new PromiseSource<HttpResponse>();
|
const pollCompleted = new PromiseSource<HttpResponse>();
|
||||||
let negotiateRequest!: HttpRequest;
|
let negotiateRequest!: HttpRequest;
|
||||||
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
||||||
.on("POST", "http://example.com?id=abc123", (req) => {
|
.on("POST", "http://example.com?id=123abc", (req) => {
|
||||||
// Respond from the poll with the handshake response
|
// Respond from the poll with the handshake response
|
||||||
negotiateRequest = req;
|
negotiateRequest = req;
|
||||||
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
||||||
|
|
@ -219,7 +221,7 @@ describe("HubConnectionBuilder", () => {
|
||||||
const pollSent = new PromiseSource<HttpRequest>();
|
const pollSent = new PromiseSource<HttpRequest>();
|
||||||
const pollCompleted = new PromiseSource<HttpResponse>();
|
const pollCompleted = new PromiseSource<HttpResponse>();
|
||||||
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
||||||
.on("POST", "http://example.com?id=abc123", (req) => {
|
.on("POST", "http://example.com?id=123abc", (req) => {
|
||||||
// Respond from the poll with the handshake response
|
// Respond from the poll with the handshake response
|
||||||
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
||||||
return new HttpResponse(202);
|
return new HttpResponse(202);
|
||||||
|
|
@ -244,7 +246,7 @@ describe("HubConnectionBuilder", () => {
|
||||||
const pollSent = new PromiseSource<HttpRequest>();
|
const pollSent = new PromiseSource<HttpRequest>();
|
||||||
const pollCompleted = new PromiseSource<HttpResponse>();
|
const pollCompleted = new PromiseSource<HttpResponse>();
|
||||||
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
||||||
.on("POST", "http://example.com?id=abc123", (req) => {
|
.on("POST", "http://example.com?id=123abc", (req) => {
|
||||||
// Respond from the poll with the handshake response
|
// Respond from the poll with the handshake response
|
||||||
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
||||||
return new HttpResponse(202);
|
return new HttpResponse(202);
|
||||||
|
|
@ -274,7 +276,7 @@ describe("HubConnectionBuilder", () => {
|
||||||
const pollSent = new PromiseSource<HttpRequest>();
|
const pollSent = new PromiseSource<HttpRequest>();
|
||||||
const pollCompleted = new PromiseSource<HttpResponse>();
|
const pollCompleted = new PromiseSource<HttpResponse>();
|
||||||
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
const testClient = createTestClient(pollSent, pollCompleted.promise)
|
||||||
.on("POST", "http://example.com?id=abc123", (req) => {
|
.on("POST", "http://example.com?id=123abc", (req) => {
|
||||||
// Respond from the poll with the handshake response
|
// Respond from the poll with the handshake response
|
||||||
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
|
||||||
return new HttpResponse(202);
|
return new HttpResponse(202);
|
||||||
|
|
@ -413,8 +415,8 @@ function createConnectionBuilder(logger?: ILogger): HubConnectionBuilder {
|
||||||
function createTestClient(pollSent: PromiseSource<HttpRequest>, pollCompleted: Promise<HttpResponse>, negotiateResponse?: any): TestHttpClient {
|
function createTestClient(pollSent: PromiseSource<HttpRequest>, pollCompleted: Promise<HttpResponse>, negotiateResponse?: any): TestHttpClient {
|
||||||
let firstRequest = true;
|
let firstRequest = true;
|
||||||
return new TestHttpClient()
|
return new TestHttpClient()
|
||||||
.on("POST", "http://example.com/negotiate", () => negotiateResponse || longPollingNegotiateResponse)
|
.on("POST", "http://example.com/negotiate?negotiateVersion=1", () => negotiateResponse || longPollingNegotiateResponse)
|
||||||
.on("GET", /http:\/\/example.com\?id=abc123&_=.*/, (req) => {
|
.on("GET", /http:\/\/example.com\?id=123abc&_=.*/, (req) => {
|
||||||
if (firstRequest) {
|
if (firstRequest) {
|
||||||
firstRequest = false;
|
firstRequest = false;
|
||||||
return new HttpResponse(200);
|
return new HttpResponse(200);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue