Add user agent header to TS client and normalized the other clients (#14484)

This commit is contained in:
Brennan 2019-10-15 08:36:15 -07:00 committed by GitHub
parent 5df73373b5
commit d35b33f294
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 412 additions and 60 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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.
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
@ -22,22 +23,68 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
Debug.Assert(assemblyVersion != null);
var majorVersion = typeof(Constants).Assembly.GetName().Version.Major;
var minorVersion = typeof(Constants).Assembly.GetName().Version.Minor;
var os = RuntimeInformation.OSDescription;
var runtime = ".NET";
var runtimeVersion = RuntimeInformation.FrameworkDescription;
// assembly version attribute should always be present
// but in case it isn't then don't include version in user-agent
if (assemblyVersion != null)
UserAgentHeader = ConstructUserAgent(typeof(Constants).Assembly.GetName().Version, assemblyVersion?.InformationalVersion, GetOS(), runtime, runtimeVersion);
}
private static string GetOS()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
UserAgentHeader = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; {os}; {runtime}; {runtimeVersion})";
return "Windows NT";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "macOS";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "Linux";
}
else
{
UserAgentHeader = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({os}; {runtime}; {runtimeVersion})";
return "";
}
}
public static string ConstructUserAgent(Version version, string detailedVersion, string os, string runtime, string runtimeVersion)
{
var userAgent = $"Microsoft SignalR/{version.Major}.{version.Minor} (";
if (!string.IsNullOrEmpty(detailedVersion))
{
userAgent += $"{detailedVersion}";
}
else
{
userAgent += "Unknown Version";
}
if (!string.IsNullOrEmpty(os))
{
userAgent += $"; {os}";
}
else
{
userAgent += "; Unknown OS";
}
userAgent += $"; {runtime}";
if (!string.IsNullOrEmpty(runtimeVersion))
{
userAgent += $"; {runtimeVersion}";
}
else
{
userAgent += "; Unknown Runtime Version";
}
userAgent += ")";
return userAgent;
}
}
}

View File

@ -12,23 +12,40 @@ public class UserAgentHelper {
}
public static String createUserAgentString() {
return constructUserAgentString(Version.getDetailedVersion(), getOS(), "Java", getJavaVersion(), getJavaVendor());
}
public static String constructUserAgentString(String detailedVersion, String os, String runtime, String runtimeVersion, String vendor) {
StringBuilder agentBuilder = new StringBuilder("Microsoft SignalR/");
// Parsing version numbers
String detailedVersion = Version.getDetailedVersion();
agentBuilder.append(getVersion(detailedVersion));
agentBuilder.append(" (");
agentBuilder.append(detailedVersion);
agentBuilder.append("; ");
// Getting the OS name
agentBuilder.append(getOS());
agentBuilder.append("; Java; ");
// Vendor and Version
agentBuilder.append(getJavaVersion());
agentBuilder.append("; ");
agentBuilder.append(getJavaVendor());
if (!os.isEmpty()) {
agentBuilder.append(os);
} else {
agentBuilder.append("Unknown OS");
}
agentBuilder.append("; ");
agentBuilder.append(runtime);
agentBuilder.append("; ");
if (!runtimeVersion.isEmpty()) {
agentBuilder.append(runtimeVersion);
} else {
agentBuilder.append("Unknown Runtime Version");
}
agentBuilder.append("; ");
if (!vendor.isEmpty()) {
agentBuilder.append(vendor);
} else {
agentBuilder.append("Unknown Vendor");
}
agentBuilder.append(")");
return agentBuilder.toString();
@ -49,6 +66,16 @@ public class UserAgentHelper {
}
static String getOS() {
return System.getProperty("os.name");
String osName = System.getProperty("os.name").toLowerCase();
if (osName.indexOf("win") >= 0) {
return "Windows NT";
} else if (osName.contains("mac") || osName.contains("darwin")) {
return "macOS";
} else if (osName.contains("linux")) {
return "Linux";
} else {
return osName;
}
}
}

View File

@ -51,4 +51,17 @@ public class UserAgentTest {
assertEquals(handMadeUserAgent, userAgent);
}
@ParameterizedTest
@MethodSource("UserAgents")
public void UserAgentHeaderIsCorrect(String detailedVersion, String os, String runtime, String runtimeVersion, String vendor, String expected) {
assertEquals(expected, UserAgentHelper.constructUserAgentString(detailedVersion, os, runtime, runtimeVersion, vendor));
}
private static Stream<Arguments> UserAgents() {
return Stream.of(
Arguments.of("1.4.5-dev", "Windows NT", "Java", "7.0.1", "Oracle", "Microsoft SignalR/1.4 (1.4.5-dev; Windows NT; Java; 7.0.1; Oracle)"),
Arguments.of("3.1.0", "", "Java", "7.0.1", "", "Microsoft SignalR/3.1 (3.1.0; Unknown OS; Java; 7.0.1; Unknown Vendor)"),
Arguments.of("5.0.2", "macOS", "Java", "", "Android", "Microsoft SignalR/5.0 (5.0.2; macOS; Java; Unknown Runtime Version; Android)"));
}
}

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Http.Connections.Features;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Net.Http.Headers;
namespace FunctionalTests
{
@ -136,6 +137,11 @@ namespace FunctionalTests
return Context.GetHttpContext().Request.Headers["Content-Type"];
}
public string GetHeader(string headerName)
{
return Context.GetHttpContext().Request.Headers[headerName];
}
public string GetCookie(string cookieName)
{
var cookies = Context.GetHttpContext().Request.Cookies;

View File

@ -6,6 +6,7 @@
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
import { getUserAgentHeader, Platform } from "@microsoft/signalr/dist/esm/Utils";
import { eachTransport, eachTransportAndProtocolAndHttpClient, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common";
import "./LogBannerReporter";
@ -1092,6 +1093,33 @@ describe("hubConnection", () => {
}
});
eachTransport((t) => {
it("sets the user agent header", async (done) => {
const hubConnection = getConnectionBuilder(t, TESTHUBENDPOINT_URL)
.withHubProtocol(new JsonHubProtocol())
.build();
try {
await hubConnection.start();
// Check to see that the Content-Type header is set the expected value
const [name, value] = getUserAgentHeader();
const headerValue = await hubConnection.invoke("GetHeader", name);
if ((t === HttpTransportType.ServerSentEvents || t === HttpTransportType.WebSockets) && !Platform.isNode) {
expect(headerValue).toBeNull();
} else {
expect(headerValue).toEqual(value);
}
await hubConnection.stop();
done();
} catch (e) {
fail(e);
}
});
});
function getJwtToken(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const httpClient = new DefaultHttpClient(NullLogger.instance);

View File

@ -9,7 +9,7 @@ import { ILogger, LogLevel } from "./ILogger";
import { HttpTransportType, ITransport, TransferFormat } from "./ITransport";
import { LongPollingTransport } from "./LongPollingTransport";
import { ServerSentEventsTransport } from "./ServerSentEventsTransport";
import { Arg, createLogger, Platform } from "./Utils";
import { Arg, createLogger, getUserAgentHeader, Platform } from "./Utils";
import { WebSocketTransport } from "./WebSocketTransport";
/** @private */
@ -302,16 +302,17 @@ export class HttpConnection implements IConnection {
}
private async getNegotiationResponse(url: string): Promise<INegotiateResponse> {
let headers;
const headers = {};
if (this.accessTokenFactory) {
const token = await this.accessTokenFactory();
if (token) {
headers = {
["Authorization"]: `Bearer ${token}`,
};
headers[`Authorization`] = `Bearer ${token}`;
}
}
const [name, value] = getUserAgentHeader();
headers[name] = value;
const negotiateUrl = this.resolveNegotiateUrl(url);
this.logger.log(LogLevel.Debug, `Sending negotiation request: ${negotiateUrl}.`);
try {

View File

@ -6,7 +6,7 @@ import { HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport";
import { Arg, getDataDetail, sendMessage } from "./Utils";
import { Arg, getDataDetail, getUserAgentHeader, sendMessage } from "./Utils";
// Not exported from 'index', this type is internal.
/** @private */
@ -58,9 +58,13 @@ 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 pollOptions: HttpRequest = {
abortSignal: this.pollAbort.signal,
headers: {},
headers,
timeout: 100000,
};
@ -194,8 +198,12 @@ export class LongPollingTransport implements ITransport {
// Send DELETE to clean up long polling on the server
this.logger.log(LogLevel.Trace, `(LongPolling transport) sending DELETE request to ${this.url}.`);
const headers = {};
const [name, value] = getUserAgentHeader();
headers[name] = value;
const deleteOptions: HttpRequest = {
headers: {},
headers,
};
const token = await this.getAccessToken();
this.updateHeaderToken(deleteOptions, token);

View File

@ -5,7 +5,7 @@ import { HttpClient } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport";
import { EventSourceConstructor } from "./Polyfills";
import { Arg, getDataDetail, Platform, sendMessage } from "./Utils";
import { Arg, getDataDetail, getUserAgentHeader, Platform, sendMessage } from "./Utils";
/** @private */
export class ServerSentEventsTransport implements ITransport {
@ -62,7 +62,13 @@ export class ServerSentEventsTransport implements ITransport {
} else {
// Non-browser passes cookies via the dictionary
const cookies = this.httpClient.getCookieString(url);
eventSource = new this.eventSourceConstructor(url, { withCredentials: true, headers: { Cookie: cookies } } as EventSourceInit);
const headers = {
Cookie: cookies,
};
const [name, value] = getUserAgentHeader();
headers[name] = value;
eventSource = new this.eventSourceConstructor(url, { withCredentials: true, headers } as EventSourceInit);
}
try {

View File

@ -7,6 +7,10 @@ import { NullLogger } from "./Loggers";
import { IStreamSubscriber, ISubscription } from "./Stream";
import { Subject } from "./Subject";
// Version token that will be replaced by the prepack command
/** The version of the SignalR client. */
export const VERSION: string = "0.0.0-DEV_BUILD";
/** @private */
export class Arg {
public static isRequired(val: any, name: string): void {
@ -25,7 +29,6 @@ export class Arg {
/** @private */
export class Platform {
public static get isBrowser(): boolean {
return typeof window === "object";
}
@ -82,7 +85,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): Promise<void> {
let headers;
let headers = {};
if (accessTokenFactory) {
const token = await accessTokenFactory();
if (token) {
@ -92,6 +95,9 @@ export async function sendMessage(logger: ILogger, transportName: string, httpCl
}
}
const [name, value] = getUserAgentHeader();
headers[name] = value;
logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content, logMessageContent)}.`);
const responseType = isArrayBuffer(content) ? "arraybuffer" : "text";
@ -181,3 +187,71 @@ export class ConsoleLogger implements ILogger {
}
}
}
/** @private */
export function getUserAgentHeader(): [string, string] {
let userAgentHeaderName = "X-SignalR-User-Agent";
if (Platform.isNode) {
userAgentHeaderName = "User-Agent";
}
return [ userAgentHeaderName, constructUserAgent(VERSION, getOsName(), getRuntime(), getRuntimeVersion()) ];
}
/** @private */
export function constructUserAgent(version: string, os: string, runtime: string, runtimeVersion: string | undefined): string {
// Microsoft SignalR/[Version] ([Detailed Version]; [Operating System]; [Runtime]; [Runtime Version])
let userAgent: string = "Microsoft SignalR/";
const majorAndMinor = version.split(".");
userAgent += `${majorAndMinor[0]}.${majorAndMinor[1]}`;
userAgent += ` (${version}; `;
if (os && os !== "") {
userAgent += `${os}; `;
} else {
userAgent += "Unknown OS; ";
}
userAgent += `${runtime}`;
if (runtimeVersion) {
userAgent += `; ${runtimeVersion}`;
} else {
userAgent += "; Unknown Runtime Version";
}
userAgent += ")";
return userAgent;
}
function getOsName(): string {
if (Platform.isNode) {
switch (process.platform) {
case "win32":
return "Windows NT";
case "darwin":
return "macOS";
case "linux":
return "Linux";
default:
return process.platform;
}
} else {
return "";
}
}
function getRuntimeVersion(): string | undefined {
if (Platform.isNode) {
return process.versions.node;
}
return undefined;
}
function getRuntime(): string {
if (Platform.isNode) {
return "NodeJS";
} else {
return "Browser";
}
}

View File

@ -5,7 +5,7 @@ import { HttpClient } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport";
import { WebSocketConstructor } from "./Polyfills";
import { Arg, getDataDetail, Platform } from "./Utils";
import { Arg, getDataDetail, getUserAgentHeader, Platform } from "./Utils";
/** @private */
export class WebSocketTransport implements ITransport {
@ -50,12 +50,18 @@ export class WebSocketTransport implements ITransport {
let webSocket: WebSocket | undefined;
const cookies = this.httpClient.getCookieString(url);
if (Platform.isNode && cookies) {
if (Platform.isNode) {
const headers = {};
const [name, value] = getUserAgentHeader();
headers[name] = value;
if (cookies) {
headers[`Cookie`] = `${cookies}`;
}
// Only pass cookies when in non-browser environments
webSocket = new this.webSocketConstructor(url, undefined, {
headers: {
Cookie: `${cookies}`,
},
headers,
});
}

View File

@ -1,10 +1,6 @@
// 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.
// Version token that will be replaced by the prepack command
/** The version of the SignalR client. */
export const VERSION: string = "0.0.0-DEV_BUILD";
// Everything that users need to access must be exported here. Including interfaces.
export { AbortSignal } from "./AbortController";
export { AbortError, HttpError, TimeoutError } from "./Errors";
@ -22,3 +18,4 @@ export { NullLogger } from "./Loggers";
export { JsonHubProtocol } from "./JsonHubProtocol";
export { Subject } from "./Subject";
export { IRetryPolicy, RetryContext } from "./IRetryPolicy";
export { VERSION } from "./Utils";

View File

@ -5,6 +5,7 @@ import { HttpResponse } from "../src/HttpClient";
import { HttpConnection, INegotiateResponse, TransportSendQueue } from "../src/HttpConnection";
import { IHttpConnectionOptions } from "../src/IHttpConnectionOptions";
import { HttpTransportType, ITransport, TransferFormat } from "../src/ITransport";
import { getUserAgentHeader } from "../src/Utils";
import { HttpError } from "../src/Errors";
import { NullLogger } from "../src/Loggers";
@ -1124,6 +1125,32 @@ describe("HttpConnection", () => {
"Failed to start the transport 'WebSockets': Error: There was an error with the transport.");
});
it("user agent header set on negotiate", async () => {
await VerifyLogger.run(async (logger) => {
let userAgentValue: string = "";
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => {
userAgentValue = r.headers![`User-Agent`];
return new HttpResponse(200, "", "{\"error\":\"nope\"}");
}),
logger,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
} catch {
} finally {
await connection.stop();
}
const [, value] = getUserAgentHeader();
expect(userAgentValue).toEqual(value);
}, "Failed to start the connection: Error: nope");
});
describe(".constructor", () => {
it("throws if no Url is provided", async () => {
// Force TypeScript to let us call the constructor incorrectly :)

View File

@ -4,6 +4,7 @@
import { HttpResponse } from "../src/HttpClient";
import { TransferFormat } from "../src/ITransport";
import { LongPollingTransport } from "../src/LongPollingTransport";
import { getUserAgentHeader } from "../src/Utils";
import { VerifyLogger } from "./Common";
import { TestHttpClient } from "./TestHttpClient";
@ -120,6 +121,50 @@ describe("LongPollingTransport", () => {
await stopPromise;
});
});
it("user agent header set on sends and polls", async () => {
await VerifyLogger.run(async (logger) => {
let firstPoll = true;
let firstPollUserAgent = "";
let secondPollUserAgent = "";
let deleteUserAgent = "";
const pollingPromiseSource = new PromiseSource();
const httpClient = new TestHttpClient()
.on("GET", async (r) => {
if (firstPoll) {
firstPoll = false;
firstPollUserAgent = r.headers![`User-Agent`];
return new HttpResponse(200);
} else {
secondPollUserAgent = r.headers![`User-Agent`];
await pollingPromiseSource.promise;
return new HttpResponse(204);
}
})
.on("DELETE", async (r) => {
deleteUserAgent = r.headers![`User-Agent`];
return new HttpResponse(202);
});
const transport = new LongPollingTransport(httpClient, undefined, logger, false);
await transport.connect("http://tempuri.org", TransferFormat.Text);
// Begin stopping transport
const stopPromise = transport.stop();
// Allow polling to complete
pollingPromiseSource.resolve();
// Wait for stop to complete
await stopPromise;
const [, value] = getUserAgentHeader();
expect(firstPollUserAgent).toEqual(value);
expect(deleteUserAgent).toEqual(value);
expect(secondPollUserAgent).toEqual(value);
});
});
});
function makeClosedPromise(transport: LongPollingTransport): Promise<void> {

View File

@ -6,6 +6,7 @@ import { TransferFormat } from "../src/ITransport";
import { HttpClient, HttpRequest } from "../src/HttpClient";
import { ILogger } from "../src/ILogger";
import { ServerSentEventsTransport } from "../src/ServerSentEventsTransport";
import { getUserAgentHeader } from "../src/Utils";
import { VerifyLogger } from "./Common";
import { TestEventSource, TestMessageEvent } from "./TestEventSource";
import { TestHttpClient } from "./TestHttpClient";
@ -179,6 +180,26 @@ describe("ServerSentEventsTransport", () => {
expect(error).toEqual(new Error("error parsing"));
});
});
it("sets user agent header on connect and 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", undefined, httpClient);
let [, value] = getUserAgentHeader();
expect((TestEventSource.eventSource.eventSourceInitDict as any).headers[`User-Agent`]).toEqual(value);
await sse.send("");
[, value] = getUserAgentHeader();
expect(request!.headers![`User-Agent`]).toBe(value);
expect(request!.url).toBe("http://example.com");
});
});
});
async function createAndStartSSE(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), httpClient?: HttpClient): Promise<ServerSentEventsTransport> {

View File

@ -11,6 +11,7 @@ export class TestEventSource {
public onmessage!: (evt: MessageEvent) => any;
public readyState: number = 0;
public url: string = "";
public eventSourceInitDict?: EventSourceInit;
public withCredentials: boolean = false;
// tslint:disable-next-line:variable-name
@ -31,6 +32,7 @@ export class TestEventSource {
constructor(url: string, eventSourceInitDict?: EventSourceInit) {
this.url = url;
this.eventSourceInitDict = eventSourceInitDict;
TestEventSource.eventSource = this;

View File

@ -12,6 +12,7 @@ export class TestWebSocket {
public protocol: string;
public readyState: number = 1;
public url: string;
public options?: any;
public static webSocketSet: PromiseSource;
public static webSocket: TestWebSocket;
@ -67,10 +68,11 @@ export class TestWebSocket {
throw new Error("Method not implemented.");
}
constructor(url: string, protocols?: string | string[]) {
constructor(url: string, protocols?: string | string[], options?: any) {
this.url = url;
this.protocol = protocols ? (typeof protocols === "string" ? protocols : protocols[0]) : "";
this.receivedData = [];
this.options = options;
TestWebSocket.webSocket = this;

View File

@ -0,0 +1,17 @@
// 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 { constructUserAgent } from "../src/Utils";
describe("User Agent", () => {
[["1.0.4-build.10", "Linux", "NodeJS", "10", "Microsoft SignalR/1.0 (1.0.4-build.10; Linux; NodeJS; 10)"],
["1.4.7-build.10", "", "Browser", "", "Microsoft SignalR/1.4 (1.4.7-build.10; Unknown OS; Browser; Unknown Runtime Version)"],
["3.1.1-build.10", "macOS", "Browser", "", "Microsoft SignalR/3.1 (3.1.1-build.10; macOS; Browser; Unknown Runtime Version)"],
["3.1.3-build.10", "", "Browser", "4", "Microsoft SignalR/3.1 (3.1.3-build.10; Unknown OS; Browser; 4)"]]
.forEach(([version, os, runtime, runtimeVersion, expected]) => {
it(`is in correct format`, async () => {
const userAgent = constructUserAgent(version, os, runtime, runtimeVersion);
expect(userAgent).toBe(expected);
});
});
});

View File

@ -3,6 +3,7 @@
import { ILogger } from "../src/ILogger";
import { TransferFormat } from "../src/ITransport";
import { getUserAgentHeader } from "../src/Utils";
import { WebSocketTransport } from "../src/WebSocketTransport";
import { VerifyLogger } from "./Common";
import { TestMessageEvent } from "./TestEventSource";
@ -202,6 +203,32 @@ describe("WebSocketTransport", () => {
});
});
});
it("sets user agent header on connect", async () => {
await VerifyLogger.run(async (logger) => {
(global as any).ErrorEvent = TestEvent;
const webSocket = await createAndStartWebSocket(logger);
let closeCalled: boolean = false;
let error: Error;
webSocket.onclose = (e) => {
closeCalled = true;
error = e!;
};
const [, value] = getUserAgentHeader();
expect(TestWebSocket.webSocket.options!.headers[`User-Agent`]).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");
});
});
});
async function createAndStartWebSocket(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), format?: TransferFormat): Promise<WebSocketTransport> {

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
@ -12,23 +14,19 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
{
public class UserAgentHeaderTest
{
[Fact]
public void UserAgentHeaderIsAccurate()
[Theory]
[MemberData(nameof(UserAgents))]
public void UserAgentHeaderIsCorrect(Version version, string detailedVersion, string os, string runtime, string runtimeVersion, string expected)
{
var majorVersion = typeof(HttpConnection).Assembly.GetName().Version.Major;
var minorVersion = typeof(HttpConnection).Assembly.GetName().Version.Minor;
var version = typeof(HttpConnection).Assembly.GetName().Version;
var os = RuntimeInformation.OSDescription;
var runtime = ".NET";
var runtimeVersion = RuntimeInformation.FrameworkDescription;
var assemblyVersion = typeof(Constants)
.Assembly
.GetCustomAttributes<AssemblyInformationalVersionAttribute>()
.FirstOrDefault();
var userAgent = Constants.UserAgentHeader;
var expectedUserAgent = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; {os}; {runtime}; {runtimeVersion})";
Assert.Equal(expected, Constants.ConstructUserAgent(version, detailedVersion, os, runtime, runtimeVersion));
}
Assert.Equal(expectedUserAgent, userAgent);
public static IEnumerable<object[]> UserAgents()
{
yield return new object[] { new Version(1, 4), "1.4.3-preview9", "Windows NT", ".NET", ".NET 4.8.7", "Microsoft SignalR/1.4 (1.4.3-preview9; Windows NT; .NET; .NET 4.8.7)" };
yield return new object[] { new Version(3, 1), "3.1.0", "", ".NET", ".NET 4.8.9", "Microsoft SignalR/3.1 (3.1.0; Unknown OS; .NET; .NET 4.8.9)" };
yield return new object[] { new Version(3, 1), "3.1.0", "", ".NET", "", "Microsoft SignalR/3.1 (3.1.0; Unknown OS; .NET; Unknown Runtime Version)" };
yield return new object[] { new Version(3, 1), "", "Linux", ".NET", ".NET 4.5.1", "Microsoft SignalR/3.1 (Unknown Version; Linux; .NET; .NET 4.5.1)" };
}
}
}