Add VerifyLogger to JS tests (#2472)

This commit is contained in:
BrennanConroy 2018-06-12 12:49:15 -07:00 committed by GitHub
parent 7317762b29
commit cb8264321d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1672 additions and 1425 deletions

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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"; import { HttpTransportType } from "../src/ITransport";
export function eachTransport(action: (transport: HttpTransportType) => void) { 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])); 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<RegExp | string | ErrorMatchFunction>) {
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<void>, ...expectedErrors: Array<RegExp | string | ErrorMatchFunction>): Promise<void> {
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);
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ import { ILogger, LogLevel } from "../src/ILogger";
import { HttpTransportType, TransferFormat } from "../src/ITransport"; import { HttpTransportType, TransferFormat } from "../src/ITransport";
import { NullLogger } from "../src/Loggers"; import { NullLogger } from "../src/Loggers";
import { VerifyLogger } from "./Common";
import { TestHttpClient } from "./TestHttpClient"; import { TestHttpClient } from "./TestHttpClient";
import { PromiseSource } from "./Utils"; import { PromiseSource } from "./Utils";
@ -43,29 +44,32 @@ describe("HubConnectionBuilder", () => {
}); });
it("builds HubConnection with HttpConnection using provided URL", async () => { it("builds HubConnection with HttpConnection using provided URL", async () => {
const pollSent = new PromiseSource<HttpRequest>(); await VerifyLogger.run(async (logger) => {
const pollCompleted = new PromiseSource<HttpResponse>(); const pollSent = new PromiseSource<HttpRequest>();
const testClient = createTestClient(pollSent, pollCompleted.promise) const pollCompleted = new PromiseSource<HttpResponse>();
.on("POST", "http://example.com?id=abc123", (req) => { const testClient = createTestClient(pollSent, pollCompleted.promise)
// Respond from the poll with the handshake response .on("POST", "http://example.com?id=abc123", (req) => {
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}")); // Respond from the poll with the handshake response
return new HttpResponse(202); pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
}); return new HttpResponse(202);
const connection = createConnectionBuilder() });
.withUrl("http://example.com", { const connection = createConnectionBuilder()
...commonHttpOptions, .withUrl("http://example.com", {
httpClient: testClient, ...commonHttpOptions,
}) httpClient: testClient,
.build(); logger,
})
.build();
// Start the connection // Start the connection
const closed = makeClosedPromise(connection); const closed = makeClosedPromise(connection);
await connection.start(); await connection.start();
const pollRequest = await pollSent.promise; const pollRequest = await pollSent.promise;
expect(pollRequest.url).toMatch(/http:\/\/example.com\?id=abc123.*/); expect(pollRequest.url).toMatch(/http:\/\/example.com\?id=abc123.*/);
await closed; await closed;
});
}); });
it("can configure transport type", async () => { it("can configure transport type", async () => {
@ -78,35 +82,38 @@ describe("HubConnectionBuilder", () => {
}); });
it("can configure hub protocol", async () => { it("can configure hub protocol", async () => {
const protocol = new TestProtocol(); await VerifyLogger.run(async (logger) => {
const protocol = new TestProtocol();
const pollSent = new PromiseSource<HttpRequest>(); const pollSent = new PromiseSource<HttpRequest>();
const pollCompleted = new PromiseSource<HttpResponse>(); const pollCompleted = new PromiseSource<HttpResponse>();
const negotiateReceived = new PromiseSource<HttpRequest>(); const negotiateReceived = new PromiseSource<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=abc123", (req) => {
// Respond from the poll with the handshake response // Respond from the poll with the handshake response
negotiateReceived.resolve(req); negotiateReceived.resolve(req);
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}")); pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
return new HttpResponse(202); return new HttpResponse(202);
}); });
const connection = createConnectionBuilder() const connection = createConnectionBuilder()
.withUrl("http://example.com", { .withUrl("http://example.com", {
...commonHttpOptions, ...commonHttpOptions,
httpClient: testClient, httpClient: testClient,
}) logger,
.withHubProtocol(protocol) })
.build(); .withHubProtocol(protocol)
.build();
// Start the connection // Start the connection
const closed = makeClosedPromise(connection); const closed = makeClosedPromise(connection);
await connection.start(); await connection.start();
const negotiateRequest = await negotiateReceived.promise; const negotiateRequest = await negotiateReceived.promise;
expect(negotiateRequest.content).toBe(`{"protocol":"${protocol.name}","version":1}\x1E`); expect(negotiateRequest.content).toBe(`{"protocol":"${protocol.name}","version":1}\x1E`);
await closed; await closed;
});
}); });
it("allows logger to be replaced", async () => { it("allows logger to be replaced", async () => {

View File

@ -3,63 +3,71 @@
import { CompletionMessage, InvocationMessage, MessageType, StreamItemMessage } from "../src/IHubProtocol"; import { CompletionMessage, InvocationMessage, MessageType, StreamItemMessage } from "../src/IHubProtocol";
import { JsonHubProtocol } from "../src/JsonHubProtocol"; import { JsonHubProtocol } from "../src/JsonHubProtocol";
import { NullLogger } from "../src/Loggers";
import { TextMessageFormat } from "../src/TextMessageFormat"; import { TextMessageFormat } from "../src/TextMessageFormat";
import { VerifyLogger } from "./Common";
describe("JsonHubProtocol", () => { describe("JsonHubProtocol", () => {
it("can write/read non-blocking Invocation message", () => { it("can write/read non-blocking Invocation message", async () => {
const invocation = { await VerifyLogger.run(async (logger) => {
arguments: [42, true, "test", ["x1", "y2"], null], const invocation = {
headers: {}, arguments: [42, true, "test", ["x1", "y2"], null],
target: "myMethod", headers: {},
type: MessageType.Invocation, target: "myMethod",
} as InvocationMessage; type: MessageType.Invocation,
} as InvocationMessage;
const protocol = new JsonHubProtocol(); const protocol = new JsonHubProtocol();
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), NullLogger.instance); const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), logger);
expect(parsedMessages).toEqual([invocation]); expect(parsedMessages).toEqual([invocation]);
});
}); });
it("can read Invocation message with Date argument", () => { it("can read Invocation message with Date argument", async () => {
const invocation = { await VerifyLogger.run(async (logger) => {
arguments: [Date.UTC(2018, 1, 1, 12, 34, 56)], const invocation = {
headers: {}, arguments: [Date.UTC(2018, 1, 1, 12, 34, 56)],
target: "mymethod", headers: {},
type: MessageType.Invocation, target: "mymethod",
} as InvocationMessage; type: MessageType.Invocation,
} as InvocationMessage;
const protocol = new JsonHubProtocol(); const protocol = new JsonHubProtocol();
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), NullLogger.instance); const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), logger);
expect(parsedMessages).toEqual([invocation]); expect(parsedMessages).toEqual([invocation]);
});
}); });
it("can write/read Invocation message with headers", () => { it("can write/read Invocation message with headers", async () => {
const invocation = { await VerifyLogger.run(async (logger) => {
arguments: [42, true, "test", ["x1", "y2"], null], const invocation = {
headers: { arguments: [42, true, "test", ["x1", "y2"], null],
foo: "bar", headers: {
}, foo: "bar",
target: "myMethod", },
type: MessageType.Invocation, target: "myMethod",
} as InvocationMessage; type: MessageType.Invocation,
} as InvocationMessage;
const protocol = new JsonHubProtocol(); const protocol = new JsonHubProtocol();
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), NullLogger.instance); const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), logger);
expect(parsedMessages).toEqual([invocation]); expect(parsedMessages).toEqual([invocation]);
});
}); });
it("can write/read Invocation message", () => { it("can write/read Invocation message", async () => {
const invocation = { await VerifyLogger.run(async (logger) => {
arguments: [42, true, "test", ["x1", "y2"], null], const invocation = {
headers: {}, arguments: [42, true, "test", ["x1", "y2"], null],
invocationId: "123", headers: {},
target: "myMethod", invocationId: "123",
type: MessageType.Invocation, target: "myMethod",
} as InvocationMessage; type: MessageType.Invocation,
} as InvocationMessage;
const protocol = new JsonHubProtocol(); const protocol = new JsonHubProtocol();
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), NullLogger.instance); const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), logger);
expect(parsedMessages).toEqual([invocation]); expect(parsedMessages).toEqual([invocation]);
});
}); });
([ ([
@ -101,9 +109,11 @@ describe("JsonHubProtocol", () => {
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } as CompletionMessage],
] as Array<[string, CompletionMessage]>).forEach(([payload, expectedMessage]) => ] as Array<[string, CompletionMessage]>).forEach(([payload, expectedMessage]) =>
it("can read Completion message", () => { it("can read Completion message", async () => {
const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); await VerifyLogger.run(async (logger) => {
expect(messages).toEqual([expectedMessage]); const messages = new JsonHubProtocol().parseMessages(payload, logger);
expect(messages).toEqual([expectedMessage]);
});
})); }));
([ ([
@ -122,9 +132,11 @@ describe("JsonHubProtocol", () => {
type: MessageType.StreamItem, type: MessageType.StreamItem,
} as StreamItemMessage], } as StreamItemMessage],
] as Array<[string, StreamItemMessage]>).forEach(([payload, expectedMessage]) => ] as Array<[string, StreamItemMessage]>).forEach(([payload, expectedMessage]) =>
it("can read StreamItem message", () => { it("can read StreamItem message", async () => {
const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); await VerifyLogger.run(async (logger) => {
expect(messages).toEqual([expectedMessage]); const messages = new JsonHubProtocol().parseMessages(payload, logger);
expect(messages).toEqual([expectedMessage]);
});
})); }));
([ ([
@ -138,9 +150,11 @@ describe("JsonHubProtocol", () => {
type: MessageType.StreamItem, type: MessageType.StreamItem,
} as StreamItemMessage], } as StreamItemMessage],
] as Array<[string, StreamItemMessage]>).forEach(([payload, expectedMessage]) => ] as Array<[string, StreamItemMessage]>).forEach(([payload, expectedMessage]) =>
it("can read message with headers", () => { it("can read message with headers", async () => {
const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); await VerifyLogger.run(async (logger) => {
expect(messages).toEqual([expectedMessage]); 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 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."], ["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]) => ] as Array<[string, string, string]>).forEach(([name, payload, expectedError]) =>
it("throws for " + name, () => { it("throws for " + name, async () => {
expect(() => new JsonHubProtocol().parseMessages(payload, NullLogger.instance)) await VerifyLogger.run(async (logger) => {
.toThrow(expectedError); expect(() => new JsonHubProtocol().parseMessages(payload, logger))
.toThrow(expectedError);
});
})); }));
it("can read multiple messages", () => { it("can read multiple messages", async () => {
const payload = `{"type":2, "invocationId": "abc", "headers": {}, "item": 8}${TextMessageFormat.RecordSeparator}{"type":3, "invocationId": "abc", "headers": {}, "result": "OK"}${TextMessageFormat.RecordSeparator}`; await VerifyLogger.run(async (logger) => {
const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); const payload = `{"type":2, "invocationId": "abc", "headers": {}, "item": 8}${TextMessageFormat.RecordSeparator}{"type":3, "invocationId": "abc", "headers": {}, "result": "OK"}${TextMessageFormat.RecordSeparator}`;
expect(messages).toEqual([ const messages = new JsonHubProtocol().parseMessages(payload, logger);
{ expect(messages).toEqual([
headers: {}, {
invocationId: "abc", headers: {},
item: 8, invocationId: "abc",
type: MessageType.StreamItem, item: 8,
} as StreamItemMessage, type: MessageType.StreamItem,
{ } as StreamItemMessage,
headers: {}, {
invocationId: "abc", headers: {},
result: "OK", invocationId: "abc",
type: MessageType.Completion, result: "OK",
} as CompletionMessage, type: MessageType.Completion,
]); } as CompletionMessage,
]);
});
}); });
it("can read ping message", () => { it("can read ping message", async () => {
const payload = `{"type":6}${TextMessageFormat.RecordSeparator}`; await VerifyLogger.run(async (logger) => {
const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); const payload = `{"type":6}${TextMessageFormat.RecordSeparator}`;
expect(messages).toEqual([ const messages = new JsonHubProtocol().parseMessages(payload, logger);
{ expect(messages).toEqual([
type: MessageType.Ping, {
}, type: MessageType.Ping,
]); },
]);
});
}); });
}); });

View File

@ -3,47 +3,56 @@
import { HttpResponse } from "../src/HttpClient"; import { HttpResponse } from "../src/HttpClient";
import { TransferFormat } from "../src/ITransport"; import { TransferFormat } from "../src/ITransport";
import { NullLogger } from "../src/Loggers";
import { LongPollingTransport } from "../src/LongPollingTransport"; import { LongPollingTransport } from "../src/LongPollingTransport";
import { VerifyLogger } from "./Common";
import { TestHttpClient } from "./TestHttpClient"; import { TestHttpClient } from "./TestHttpClient";
import { PromiseSource, SyncPoint } from "./Utils"; import { PromiseSource, SyncPoint } from "./Utils";
describe("LongPollingTransport", () => { describe("LongPollingTransport", () => {
it("shuts down polling by aborting in-progress request", async () => { it("shuts down polling by aborting in-progress request", async () => {
let firstPoll = true; await VerifyLogger.run(async (logger) => {
const pollCompleted = new PromiseSource(); let firstPoll = true;
const client = new TestHttpClient() const pollCompleted = new PromiseSource();
.on("GET", async (r) => { const client = new TestHttpClient()
if (firstPoll) { .on("GET", async (r) => {
firstPoll = false; if (firstPoll) {
return new HttpResponse(200); firstPoll = false;
} else { return new HttpResponse(200);
// Turn 'onabort' into a promise. } else {
const abort = new Promise((resolve, reject) => r.abortSignal!.onabort = resolve); // Turn 'onabort' into a promise.
await abort; const abort = new Promise((resolve, reject) => {
if (r.abortSignal!.aborted) {
resolve();
} else {
r.abortSignal!.onabort = resolve;
}
});
await abort;
// Signal that the poll has completed. // Signal that the poll has completed.
pollCompleted.resolve(); pollCompleted.resolve();
return new HttpResponse(200); return new HttpResponse(200);
} }
}) })
.on("DELETE", () => new HttpResponse(202)); .on("DELETE", () => new HttpResponse(202));
const transport = new LongPollingTransport(client, undefined, NullLogger.instance, false); const transport = new LongPollingTransport(client, undefined, logger, false);
await transport.connect("http://example.com", TransferFormat.Text); await transport.connect("http://example.com", TransferFormat.Text);
const stopPromise = transport.stop(); const stopPromise = transport.stop();
await pollCompleted.promise; await pollCompleted.promise;
await stopPromise; await stopPromise;
});
}); });
it("204 server response stops polling and raises onClose", async () => { it("204 server response stops polling and raises onClose", async () => {
let firstPoll = true; await VerifyLogger.run(async (logger) => {
const client = new TestHttpClient() let firstPoll = true;
.on("GET", async () => { const client = new TestHttpClient()
.on("GET", async () => {
if (firstPoll) { if (firstPoll) {
firstPoll = false; firstPoll = false;
return new HttpResponse(200); return new HttpResponse(200);
@ -52,59 +61,62 @@ describe("LongPollingTransport", () => {
return new HttpResponse(204); 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 // Close will be called on transport because of 204 result from polling
await stopPromise; await stopPromise;
});
}); });
it("sends DELETE on stop after polling has finished", async () => { it("sends DELETE on stop after polling has finished", async () => {
let firstPoll = true; await VerifyLogger.run(async (logger) => {
let deleteSent = false; let firstPoll = true;
const pollingPromiseSource = new PromiseSource(); let deleteSent = false;
const deleteSyncPoint = new SyncPoint(); const pollingPromiseSource = new PromiseSource();
const httpClient = new TestHttpClient() const deleteSyncPoint = new SyncPoint();
.on("GET", async (r) => { const httpClient = new TestHttpClient()
if (firstPoll) { .on("GET", async (r) => {
firstPoll = false; if (firstPoll) {
return new HttpResponse(200); firstPoll = false;
} else { return new HttpResponse(200);
await pollingPromiseSource.promise; } else {
return new HttpResponse(204); await pollingPromiseSource.promise;
} return new HttpResponse(204);
}) }
.on("DELETE", async (r) => { })
deleteSent = true; .on("DELETE", async (r) => {
await deleteSyncPoint.waitToContinue(); deleteSent = true;
return new HttpResponse(202); 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 // Begin stopping transport
const stopPromise = transport.stop(); const stopPromise = transport.stop();
// Delete will not be sent until polling is finished // Delete will not be sent until polling is finished
expect(deleteSent).toEqual(false); expect(deleteSent).toEqual(false);
// Allow polling to complete // Allow polling to complete
pollingPromiseSource.resolve(); pollingPromiseSource.resolve();
// Wait for delete to be called // Wait for delete to be called
await deleteSyncPoint.waitForSyncPoint(); await deleteSyncPoint.waitForSyncPoint();
expect(deleteSent).toEqual(true); expect(deleteSent).toEqual(true);
deleteSyncPoint.continue(); deleteSyncPoint.continue();
// Wait for stop to complete // Wait for stop to complete
await stopPromise; await stopPromise;
});
}); });
}); });