Add JS unit tests for SSE (#2484)
This commit is contained in:
parent
adc07e74a9
commit
296fcc9423
|
|
@ -38,6 +38,9 @@ export class ServerSentEventsTransport implements ITransport {
|
||||||
|
|
||||||
this.logger.log(LogLevel.Trace, "(SSE transport) Connecting");
|
this.logger.log(LogLevel.Trace, "(SSE transport) Connecting");
|
||||||
|
|
||||||
|
// set url before accessTokenFactory because this.url is only for send and we set the auth header instead of the query string for send
|
||||||
|
this.url = url;
|
||||||
|
|
||||||
if (this.accessTokenFactory) {
|
if (this.accessTokenFactory) {
|
||||||
const token = await this.accessTokenFactory();
|
const token = await this.accessTokenFactory();
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|
@ -45,11 +48,11 @@ export class ServerSentEventsTransport implements ITransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.url = url;
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
let opened = false;
|
let opened = false;
|
||||||
if (transferFormat !== TransferFormat.Text) {
|
if (transferFormat !== TransferFormat.Text) {
|
||||||
reject(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"));
|
reject(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventSource = new this.eventSourceConstructor(url, { withCredentials: true });
|
const eventSource = new this.eventSourceConstructor(url, { withCredentials: true });
|
||||||
|
|
@ -61,16 +64,14 @@ export class ServerSentEventsTransport implements ITransport {
|
||||||
this.logger.log(LogLevel.Trace, `(SSE transport) data received. ${getDataDetail(e.data, this.logMessageContent)}.`);
|
this.logger.log(LogLevel.Trace, `(SSE transport) data received. ${getDataDetail(e.data, this.logMessageContent)}.`);
|
||||||
this.onreceive(e.data);
|
this.onreceive(e.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.onclose) {
|
this.close(error);
|
||||||
this.onclose(error);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onerror = (e: any) => {
|
eventSource.onerror = (e: MessageEvent) => {
|
||||||
const error = new Error(e.message || "Error occurred");
|
const error = new Error(e.data || "Error occurred");
|
||||||
if (opened) {
|
if (opened) {
|
||||||
this.close(error);
|
this.close(error);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -85,7 +86,8 @@ export class ServerSentEventsTransport implements ITransport {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Promise.reject(e);
|
reject(e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export class WebSocketTransport implements ITransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
public send(data: any): Promise<void> {
|
public send(data: any): Promise<void> {
|
||||||
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
if (this.webSocket && this.webSocket.readyState === this.webSocketConstructor.OPEN) {
|
||||||
this.logger.log(LogLevel.Trace, `(WebSockets transport) sending data. ${getDataDetail(data, this.logMessageContent)}.`);
|
this.logger.log(LogLevel.Trace, `(WebSockets transport) sending data. ${getDataDetail(data, this.logMessageContent)}.`);
|
||||||
this.webSocket.send(data);
|
this.webSocket.send(data);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|
|
||||||
|
|
@ -747,7 +747,7 @@ describe("HttpConnection", () => {
|
||||||
|
|
||||||
await expect(connection.start(TransferFormat.Text))
|
await expect(connection.start(TransferFormat.Text))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Unable to initialize any of the available transports.");
|
.toEqual(new Error("Unable to initialize any of the available transports."));
|
||||||
|
|
||||||
expect(eventSourceConstructorCalled).toEqual(true);
|
expect(eventSourceConstructorCalled).toEqual(true);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
// 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 { TransferFormat } from "../src/ITransport";
|
||||||
|
|
||||||
|
import { HttpClient, HttpRequest } from "../src/HttpClient";
|
||||||
|
import { ILogger } from "../src/ILogger";
|
||||||
|
import { ServerSentEventsTransport } from "../src/ServerSentEventsTransport";
|
||||||
|
import { VerifyLogger } from "./Common";
|
||||||
|
import { TestEventSource, TestMessageEvent } from "./TestEventSource";
|
||||||
|
import { TestHttpClient } from "./TestHttpClient";
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
await expect(sse.connect("", TransferFormat.Binary))
|
||||||
|
.rejects
|
||||||
|
.toEqual(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("connect waits for EventSource to be connected", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource);
|
||||||
|
|
||||||
|
let connectComplete: boolean = false;
|
||||||
|
const connectPromise = (async () => {
|
||||||
|
await sse.connect("http://example.com", TransferFormat.Text);
|
||||||
|
connectComplete = true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
await TestEventSource.eventSource.openSet;
|
||||||
|
|
||||||
|
expect(connectComplete).toEqual(false);
|
||||||
|
|
||||||
|
TestEventSource.eventSource.onopen(new TestMessageEvent());
|
||||||
|
|
||||||
|
await connectPromise;
|
||||||
|
expect(connectComplete).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[["http://example.com", "http://example.com?access_token=secretToken"],
|
||||||
|
["http://example.com?value=null", "http://example.com?value=null&access_token=secretToken"]]
|
||||||
|
.forEach(([input, expected]) => {
|
||||||
|
it(`appends access_token to url ${input}`, async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
await createAndStartSSE(logger, input, () => "secretToken");
|
||||||
|
|
||||||
|
expect(TestEventSource.eventSource.url).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets Authorization header on sends", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
let request: HttpRequest;
|
||||||
|
const httpClient = new TestHttpClient().on((r) => {
|
||||||
|
request = r;
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const sse = await createAndStartSSE(logger, "http://example.com", () => "secretToken", httpClient);
|
||||||
|
|
||||||
|
await sse.send("");
|
||||||
|
|
||||||
|
expect(request!.headers!.Authorization).toEqual("Bearer secretToken");
|
||||||
|
expect(request!.url).toEqual("http://example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can send data", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
let request: HttpRequest;
|
||||||
|
const httpClient = new TestHttpClient().on((r) => {
|
||||||
|
request = r;
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const sse = await createAndStartSSE(logger, "http://example.com", undefined, httpClient);
|
||||||
|
|
||||||
|
await sse.send("send data");
|
||||||
|
|
||||||
|
expect(request!.content).toEqual("send data");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can receive data", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const sse = await createAndStartSSE(logger);
|
||||||
|
|
||||||
|
let received: string | ArrayBuffer;
|
||||||
|
sse.onreceive = (data) => {
|
||||||
|
received = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const message = new TestMessageEvent();
|
||||||
|
message.data = "receive data";
|
||||||
|
TestEventSource.eventSource.onmessage(message);
|
||||||
|
|
||||||
|
expect(typeof received!).toEqual("string");
|
||||||
|
expect(received!).toEqual("receive data");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stop closes EventSource and calls onclose", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const sse = await createAndStartSSE(logger);
|
||||||
|
|
||||||
|
let closeCalled: boolean = false;
|
||||||
|
sse.onclose = () => {
|
||||||
|
closeCalled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
await sse.stop();
|
||||||
|
|
||||||
|
expect(closeCalled).toEqual(true);
|
||||||
|
expect(TestEventSource.eventSource.closed).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can close from EventSource error", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const sse = await createAndStartSSE(logger);
|
||||||
|
|
||||||
|
let closeCalled: boolean = false;
|
||||||
|
let error: Error | undefined;
|
||||||
|
sse.onclose = (e) => {
|
||||||
|
closeCalled = true;
|
||||||
|
error = e;
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorEvent = new TestMessageEvent();
|
||||||
|
errorEvent.data = "error";
|
||||||
|
TestEventSource.eventSource.onerror(errorEvent);
|
||||||
|
|
||||||
|
expect(closeCalled).toEqual(true);
|
||||||
|
expect(TestEventSource.eventSource.closed).toEqual(true);
|
||||||
|
expect(error).toMatchObject({ message: "error" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("send throws if not connected", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const sse = new ServerSentEventsTransport(new TestHttpClient(), undefined, logger, true, TestEventSource);
|
||||||
|
|
||||||
|
await expect(sse.send(""))
|
||||||
|
.rejects
|
||||||
|
.toMatchObject({ message: "Cannot send until the transport is connected" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes on error from receive", async () => {
|
||||||
|
await VerifyLogger.run(async (logger) => {
|
||||||
|
const sse = await createAndStartSSE(logger);
|
||||||
|
|
||||||
|
sse.onreceive = () => {
|
||||||
|
throw new Error("error parsing");
|
||||||
|
};
|
||||||
|
|
||||||
|
let closeCalled: boolean = false;
|
||||||
|
let error: Error | undefined;
|
||||||
|
sse.onclose = (e) => {
|
||||||
|
closeCalled = true;
|
||||||
|
error = e;
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorEvent = new TestMessageEvent();
|
||||||
|
errorEvent.data = "some data";
|
||||||
|
TestEventSource.eventSource.onmessage(errorEvent);
|
||||||
|
|
||||||
|
expect(closeCalled).toEqual(true);
|
||||||
|
expect(TestEventSource.eventSource.closed).toEqual(true);
|
||||||
|
expect(error).toMatchObject({ message: "error parsing" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const connectPromise = sse.connect(url || "http://example.com", TransferFormat.Text);
|
||||||
|
await TestEventSource.eventSource.openSet;
|
||||||
|
|
||||||
|
TestEventSource.eventSource.onopen(new TestMessageEvent());
|
||||||
|
await connectPromise;
|
||||||
|
return sse;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
import { PromiseSource } from "./Utils";
|
||||||
|
|
||||||
|
export class TestEventSource {
|
||||||
|
public CONNECTING: number = 1;
|
||||||
|
public OPEN: number = 2;
|
||||||
|
public CLOSED: number = 3;
|
||||||
|
public onerror!: (evt: MessageEvent) => any;
|
||||||
|
public onmessage!: (evt: MessageEvent) => any;
|
||||||
|
public readyState: number = 0;
|
||||||
|
public url: string = "";
|
||||||
|
public withCredentials: boolean = false;
|
||||||
|
|
||||||
|
// tslint:disable-next-line:variable-name
|
||||||
|
private _onopen?: (evt: MessageEvent) => any;
|
||||||
|
public openSet: PromiseSource = new PromiseSource();
|
||||||
|
public set onopen(value: (evt: MessageEvent) => any) {
|
||||||
|
this._onopen = value;
|
||||||
|
this.openSet.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get onopen(): (evt: MessageEvent) => any {
|
||||||
|
return this._onopen!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static eventSource: TestEventSource;
|
||||||
|
public closed: boolean = false;
|
||||||
|
|
||||||
|
constructor(url: string, eventSourceInitDict?: EventSourceInit) {
|
||||||
|
this.url = url;
|
||||||
|
TestEventSource.eventSource = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
public addEventListener(type: string, listener?: EventListener | EventListenerObject | null, options?: boolean | AddEventListenerOptions): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
public dispatchEvent(evt: Event): boolean {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
public removeEventListener(type: string, listener?: EventListener | EventListenerObject | null, options?: boolean | EventListenerOptions): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static callOnOpen(): void {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestMessageEvent {
|
||||||
|
public data: any;
|
||||||
|
public readonly origin!: string;
|
||||||
|
public readonly ports!: ReadonlyArray<MessagePort>;
|
||||||
|
public readonly source!: Window;
|
||||||
|
public initMessageEvent(type: string, bubbles: boolean, cancelable: boolean, data: any, origin: string, lastEventId: string, source: Window): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
public bubbles!: boolean;
|
||||||
|
public cancelBubble!: boolean;
|
||||||
|
public cancelable!: boolean;
|
||||||
|
public currentTarget!: EventTarget;
|
||||||
|
public defaultPrevented!: boolean;
|
||||||
|
public eventPhase!: number;
|
||||||
|
public isTrusted!: boolean;
|
||||||
|
public returnValue!: boolean;
|
||||||
|
public scoped!: boolean;
|
||||||
|
public srcElement!: Element | null;
|
||||||
|
public target!: EventTarget;
|
||||||
|
public timeStamp!: number;
|
||||||
|
public type!: string;
|
||||||
|
public deepPath(): EventTarget[] {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
public initEvent(type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
public preventDefault(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
public stopImmediatePropagation(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
public stopPropagation(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
public AT_TARGET!: number;
|
||||||
|
public BUBBLING_PHASE!: number;
|
||||||
|
public CAPTURING_PHASE!: number;
|
||||||
|
public NONE!: number;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue