diff --git a/clients/ts/FunctionalTests/ts/ConnectionTests.ts b/clients/ts/FunctionalTests/ts/ConnectionTests.ts index 05143c0938..6c5351c6fd 100644 --- a/clients/ts/FunctionalTests/ts/ConnectionTests.ts +++ b/clients/ts/FunctionalTests/ts/ConnectionTests.ts @@ -1,10 +1,13 @@ // 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 { HttpConnection, HttpTransportType, IHttpConnectionOptions, LogLevel, TransferFormat } from "@aspnet/signalr"; +import { HttpTransportType, IHttpConnectionOptions, LogLevel, TransferFormat } from "@aspnet/signalr"; import { eachTransport, ECHOENDPOINT_URL } from "./Common"; import { TestLogger } from "./TestLogger"; +// We want to continue testing HttpConnection, but we don't export it anymore. So just pull it in directly from the source file. +import { HttpConnection } from "../../signalr/src/HttpConnection"; + const commonOptions: IHttpConnectionOptions = { logMessageContent: true, logger: TestLogger.instance, diff --git a/clients/ts/FunctionalTests/ts/index.ts b/clients/ts/FunctionalTests/ts/index.ts index 6a7008345a..9fd847cb12 100644 --- a/clients/ts/FunctionalTests/ts/index.ts +++ b/clients/ts/FunctionalTests/ts/index.ts @@ -4,7 +4,6 @@ console.log("SignalR Functional Tests Loaded"); import "es6-promise/dist/es6-promise.auto.js"; -import "./ConnectionTests"; import "./HubConnectionTests"; import "./WebDriverReporter"; import "./WebSocketTests"; diff --git a/clients/ts/signalr/spec/HttpClient.spec.ts b/clients/ts/signalr/spec/HttpClient.spec.ts index f4af2473a2..7c5c272024 100644 --- a/clients/ts/signalr/spec/HttpClient.spec.ts +++ b/clients/ts/signalr/spec/HttpClient.spec.ts @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -import { HttpRequest } from "../src/index"; +import { HttpRequest } from "../src/HttpClient"; import { TestHttpClient } from "./TestHttpClient"; import { asyncit as it } from "./Utils"; diff --git a/clients/ts/signalr/spec/HttpConnection.spec.ts b/clients/ts/signalr/spec/HttpConnection.spec.ts index 6c45e76213..853faf739c 100644 --- a/clients/ts/signalr/spec/HttpConnection.spec.ts +++ b/clients/ts/signalr/spec/HttpConnection.spec.ts @@ -1,9 +1,9 @@ // 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 { HttpResponse } from "../src/HttpClient"; import { HttpConnection } from "../src/HttpConnection"; -import { IHttpConnectionOptions } from "../src/HttpConnection"; -import { HttpResponse } from "../src/index"; +import { IHttpConnectionOptions } from "../src/IHttpConnectionOptions"; import { HttpTransportType, ITransport, TransferFormat } from "../src/ITransport"; import { eachEndpointUrl, eachTransport } from "./Common"; import { TestHttpClient } from "./TestHttpClient"; @@ -435,16 +435,26 @@ describe("HttpConnection", () => { it("authorization header removed when token factory returns null and using LongPolling", async (done) => { const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] }; - var httpClientGetCount = 0; - var accessTokenFactoryCount = 0; + let httpClientGetCount = 0; + let accessTokenFactoryCount = 0; const options: IHttpConnectionOptions = { ...commonOptions, + accessTokenFactory: () => { + accessTokenFactoryCount++; + if (accessTokenFactoryCount === 1) { + return "A token value"; + } else { + // Return a null value after the first call to test the header being removed + return null; + } + }, httpClient: new TestHttpClient() .on("POST", (r) => ({ connectionId: "42", availableTransports: [availableTransport] })) .on("GET", (r) => { httpClientGetCount++; + // tslint:disable-next-line:no-string-literal const authorizationValue = r.headers["Authorization"]; - if (httpClientGetCount == 1) { + if (httpClientGetCount === 1) { if (authorizationValue) { fail("First long poll request should have a authorization header."); } @@ -458,15 +468,6 @@ describe("HttpConnection", () => { throw new Error("fail"); } }), - accessTokenFactory: () => { - accessTokenFactoryCount++; - if (accessTokenFactoryCount == 1) { - return "A token value"; - } else { - // Return a null value after the first call to test the header being removed - return null; - } - }, } as IHttpConnectionOptions; const connection = new HttpConnection("http://tempuri.org", options); @@ -485,14 +486,14 @@ describe("HttpConnection", () => { it("sets inherentKeepAlive feature when using LongPolling", async (done) => { const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] }; - var httpClientGetCount = 0; + let httpClientGetCount = 0; const options: IHttpConnectionOptions = { ...commonOptions, httpClient: new TestHttpClient() .on("POST", (r) => ({ connectionId: "42", availableTransports: [availableTransport] })) .on("GET", (r) => { httpClientGetCount++; - if (httpClientGetCount == 1) { + if (httpClientGetCount === 1) { // First long polling request must succeed so start completes return ""; } else { diff --git a/clients/ts/signalr/spec/HubConnection.spec.ts b/clients/ts/signalr/spec/HubConnection.spec.ts index a438bb8721..4e17b1bba4 100644 --- a/clients/ts/signalr/spec/HubConnection.spec.ts +++ b/clients/ts/signalr/spec/HubConnection.spec.ts @@ -1,11 +1,12 @@ // 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 { HubConnection, JsonHubProtocol } from "../src/HubConnection"; +import { HubConnection } from "../src/HubConnection"; import { IConnection } from "../src/IConnection"; import { HubMessage, IHubProtocol, MessageType } from "../src/IHubProtocol"; import { ILogger, LogLevel } from "../src/ILogger"; import { HttpTransportType, ITransport, TransferFormat } from "../src/ITransport"; +import { JsonHubProtocol } from "../src/JsonHubProtocol"; import { NullLogger } from "../src/Loggers"; import { IStreamSubscriber } from "../src/Stream"; import { TextMessageFormat } from "../src/TextMessageFormat"; @@ -13,7 +14,7 @@ import { TextMessageFormat } from "../src/TextMessageFormat"; import { asyncit as it, captureException, delay, PromiseSource } from "./Utils"; function createHubConnection(connection: IConnection, logger?: ILogger, protocol?: IHubProtocol) { - return new HubConnection(connection, logger || NullLogger.instance, protocol || new JsonHubProtocol()); + return HubConnection.create(connection, logger || NullLogger.instance, protocol || new JsonHubProtocol()); } describe("HubConnection", () => { diff --git a/clients/ts/signalr/spec/HubConnectionBuilder.spec.ts b/clients/ts/signalr/spec/HubConnectionBuilder.spec.ts index 45035ca414..1adbe925cd 100644 --- a/clients/ts/signalr/spec/HubConnectionBuilder.spec.ts +++ b/clients/ts/signalr/spec/HubConnectionBuilder.spec.ts @@ -3,13 +3,14 @@ import { HubConnectionBuilder } from "../src/HubConnectionBuilder"; -import { HubConnection } from "../src"; import { HttpRequest, HttpResponse } from "../src/HttpClient"; -import { IHttpConnectionOptions } from "../src/HttpConnection"; +import { HubConnection } from "../src/HubConnection"; +import { IHttpConnectionOptions } from "../src/IHttpConnectionOptions"; import { HubMessage, IHubProtocol } from "../src/IHubProtocol"; import { ILogger, LogLevel } from "../src/ILogger"; -import { TransferFormat, HttpTransportType } from "../src/ITransport"; +import { HttpTransportType, TransferFormat } from "../src/ITransport"; import { NullLogger } from "../src/Loggers"; + import { TestHttpClient } from "./TestHttpClient"; import { asyncit as it, PromiseSource } from "./Utils"; @@ -123,14 +124,13 @@ describe("HubConnectionBuilder", () => { await closed; }); - it("allows logger to be replaced", async () => { let loggedMessages = 0; const logger = { log() { loggedMessages += 1; - } - } + }, + }; const connection = createConnectionBuilder(logger) .withUrl("http://example.com") .build(); diff --git a/clients/ts/signalr/src/HttpConnection.ts b/clients/ts/signalr/src/HttpConnection.ts index 3a9e905d4a..615cb07f70 100644 --- a/clients/ts/signalr/src/HttpConnection.ts +++ b/clients/ts/signalr/src/HttpConnection.ts @@ -3,6 +3,7 @@ import { DefaultHttpClient, HttpClient } from "./HttpClient"; import { IConnection } from "./IConnection"; +import { IHttpConnectionOptions } from "./IHttpConnectionOptions"; import { ILogger, LogLevel } from "./ILogger"; import { HttpTransportType, ITransport, TransferFormat } from "./ITransport"; import { LongPollingTransport } from "./LongPollingTransport"; @@ -10,15 +11,6 @@ import { ServerSentEventsTransport } from "./ServerSentEventsTransport"; import { Arg, createLogger } from "./Utils"; import { WebSocketTransport } from "./WebSocketTransport"; -export interface IHttpConnectionOptions { - httpClient?: HttpClient; - transport?: HttpTransportType | ITransport; - logger?: ILogger | LogLevel; - accessTokenFactory?: () => string | Promise; - logMessageContent?: boolean; - skipNegotiation?: boolean; -} - const enum ConnectionState { Connecting, Connected, diff --git a/clients/ts/signalr/src/HubConnection.ts b/clients/ts/signalr/src/HubConnection.ts index 1ed9ab125e..875217ded9 100644 --- a/clients/ts/signalr/src/HubConnection.ts +++ b/clients/ts/signalr/src/HubConnection.ts @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. import { HandshakeProtocol, HandshakeRequestMessage, HandshakeResponseMessage } from "./HandshakeProtocol"; -import { HttpConnection, IHttpConnectionOptions } from "./HttpConnection"; +import { HttpConnection } from "./HttpConnection"; import { IConnection } from "./IConnection"; import { CancelInvocationMessage, CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageType, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol"; import { ILogger, LogLevel } from "./ILogger"; @@ -12,8 +12,6 @@ import { IStreamResult } from "./Stream"; import { TextMessageFormat } from "./TextMessageFormat"; import { Arg, createLogger, Subject } from "./Utils"; -export { JsonHubProtocol }; - const DEFAULT_TIMEOUT_IN_MS: number = 30 * 1000; export class HubConnection { @@ -30,7 +28,16 @@ export class HubConnection { public serverTimeoutInMilliseconds: number; - constructor(connection: IConnection, logger: ILogger, protocol: IHubProtocol) { + /** @internal */ + // Using a public static factory method means we can have a private constructor and an _internal_ + // create method that can be used by HubConnectionBuilder. An "internal" constructor would just + // be stripped away and the '.d.ts' file would have no constructor, which is interpreted as a + // public parameter-less constructor. + public static create(connection: IConnection, logger: ILogger, protocol: IHubProtocol): HubConnection { + return new HubConnection(connection, logger, protocol); + } + + private constructor(connection: IConnection, logger: ILogger, protocol: IHubProtocol) { Arg.isRequired(connection, "connection"); Arg.isRequired(logger, "logger"); Arg.isRequired(protocol, "protocol"); diff --git a/clients/ts/signalr/src/HubConnectionBuilder.ts b/clients/ts/signalr/src/HubConnectionBuilder.ts index 9dca525333..b5a4f42b93 100644 --- a/clients/ts/signalr/src/HubConnectionBuilder.ts +++ b/clients/ts/signalr/src/HubConnectionBuilder.ts @@ -1,11 +1,13 @@ // 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 { HttpConnection, IHttpConnectionOptions } from "./HttpConnection"; -import { HubConnection, JsonHubProtocol } from "./HubConnection"; +import { HttpConnection } from "./HttpConnection"; +import { HubConnection } from "./HubConnection"; +import { IHttpConnectionOptions } from "./IHttpConnectionOptions"; import { IHubProtocol } from "./IHubProtocol"; import { ILogger, LogLevel } from "./ILogger"; import { HttpTransportType } from "./ITransport"; +import { JsonHubProtocol } from "./JsonHubProtocol"; import { NullLogger } from "./Loggers"; import { Arg, ConsoleLogger } from "./Utils"; @@ -76,7 +78,7 @@ export class HubConnectionBuilder { } const connection = new HttpConnection(this.url, httpConnectionOptions); - return new HubConnection( + return HubConnection.create( connection, this.logger || NullLogger.instance, this.protocol || new JsonHubProtocol()); diff --git a/clients/ts/signalr/src/IHttpConnectionOptions.ts b/clients/ts/signalr/src/IHttpConnectionOptions.ts new file mode 100644 index 0000000000..cdb6ad73f3 --- /dev/null +++ b/clients/ts/signalr/src/IHttpConnectionOptions.ts @@ -0,0 +1,15 @@ +// 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 { HttpClient } from "./HttpClient"; +import { ILogger, LogLevel } from "./ILogger"; +import { HttpTransportType, ITransport } from "./ITransport"; + +export interface IHttpConnectionOptions { + httpClient?: HttpClient; + transport?: HttpTransportType | ITransport; + logger?: ILogger | LogLevel; + accessTokenFactory?: () => string | Promise; + logMessageContent?: boolean; + skipNegotiation?: boolean; +} diff --git a/clients/ts/signalr/src/index.ts b/clients/ts/signalr/src/index.ts index 792b68f8ae..f1aee7490f 100644 --- a/clients/ts/signalr/src/index.ts +++ b/clients/ts/signalr/src/index.ts @@ -4,12 +4,12 @@ // Everything that users need to access must be exported here. Including interfaces. export * from "./Errors"; export * from "./HttpClient"; -export * from "./HttpConnection"; +export * from "./IHttpConnectionOptions"; export * from "./HubConnection"; export * from "./HubConnectionBuilder"; -export * from "./IConnection"; export * from "./IHubProtocol"; export * from "./ILogger"; export * from "./ITransport"; export * from "./Stream"; export * from "./Loggers"; +export * from "./JsonHubProtocol"; diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubClients`T.cs b/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubClients`T.cs index 69926e9695..2b2aca344f 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubClients`T.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubClients`T.cs @@ -1,4 +1,4 @@ -// 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. using System.Collections.Generic; diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Internal/TypedClientBuilder.cs b/src/Microsoft.AspNetCore.SignalR.Core/Internal/TypedClientBuilder.cs index bcae8fec8c..7f416b9ea6 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/Internal/TypedClientBuilder.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/Internal/TypedClientBuilder.cs @@ -1,9 +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. using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -26,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal public static void Validate() { // The following will throw if T is not a valid type - _ = _builder.Value; + _ = _builder.Value; } private static Func GenerateClientBuilder() @@ -151,7 +150,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal for (var i = 0; i < paramTypes.Length; i++) { generator.Emit(OpCodes.Ldloc_0); // Object array loaded - generator.Emit(OpCodes.Ldc_I4, i); + generator.Emit(OpCodes.Ldc_I4, i); generator.Emit(OpCodes.Ldarg, i + 1); // i + 1 generator.Emit(OpCodes.Box, paramTypes[i]); generator.Emit(OpCodes.Stelem_Ref); @@ -161,13 +160,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal generator.Emit(OpCodes.Ldloc_0); generator.Emit(OpCodes.Callvirt, invokeMethod); - if (interfaceMethodInfo.ReturnType == typeof(void)) - { - // void return - generator.Emit(OpCodes.Pop); - } - - generator.Emit(OpCodes.Ret); // Return + generator.Emit(OpCodes.Ret); // Return the Task returned by 'invokeMethod' } private static void VerifyInterface(Type interfaceType) @@ -184,7 +177,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal if (interfaceType.GetEvents().Length != 0) { - throw new InvalidOperationException("Type can not contain events."); + throw new InvalidOperationException("Type must not contain events."); } foreach (var method in interfaceType.GetMethods()) @@ -200,28 +193,26 @@ namespace Microsoft.AspNetCore.SignalR.Internal private static void VerifyMethod(Type interfaceType, MethodInfo interfaceMethod) { - if (interfaceMethod.ReturnType != typeof(void) && interfaceMethod.ReturnType != typeof(Task)) + if (interfaceMethod.ReturnType != typeof(Task)) { - throw new InvalidOperationException("Method must return Void or Task."); + throw new InvalidOperationException( + $"Cannot generate proxy implementation for '{typeof(T).FullName}.{interfaceMethod.Name}'. All client proxy methods must return '{typeof(Task).FullName}'."); } foreach (var parameter in interfaceMethod.GetParameters()) { - VerifyParameter(interfaceType, interfaceMethod, parameter); - } - } + if (parameter.IsOut) + { + throw new InvalidOperationException( + $"Cannot generate proxy implementation for '{typeof(T).FullName}.{interfaceMethod.Name}'. Client proxy methods must not have 'out' parameters."); + } - private static void VerifyParameter(Type interfaceType, MethodInfo interfaceMethod, ParameterInfo parameter) - { - if (parameter.IsOut) - { - throw new InvalidOperationException("Method must not take out parameters."); - } - - if (parameter.ParameterType.IsByRef) - { - throw new InvalidOperationException("Method must not take reference parameters."); + if (parameter.ParameterType.IsByRef) + { + throw new InvalidOperationException( + $"Cannot generate proxy implementation for '{typeof(T).FullName}.{interfaceMethod.Name}'. Client proxy methods must not have 'ref' parameters."); + } } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs index 2ac5a70512..9f6189d785 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs @@ -484,6 +484,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + public class SimpleVoidReturningTypedHub : Hub + { + public override Task OnConnectedAsync() + { + // Derefernce Clients, to force initialization of the TypedHubClient + Clients.All.Send("herp"); + return Task.CompletedTask; + } + } + public class SimpleTypedHub : Hub { public override async Task OnConnectedAsync() @@ -534,6 +544,11 @@ namespace Microsoft.AspNetCore.SignalR.Tests Task Send(string message); } + public interface IVoidReturningTypedHubClient + { + void Send(string message); + } + public class ConnectionLifetimeHub : Hub { private readonly ConnectionLifetimeState _state; diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs index e3068ffd0e..ec8119b19c 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs @@ -143,6 +143,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests await context.Clients.All.Send("test"); } + [Fact] + public void FailsToLoadInvalidTypedHubClient() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var ex = Assert.Throws(() => + serviceProvider.GetRequiredService>()); + Assert.Equal($"Cannot generate proxy implementation for '{typeof(IVoidReturningTypedHubClient).FullName}.{nameof(IVoidReturningTypedHubClient.Send)}'. All client proxy methods must return '{typeof(Task).FullName}'.", ex.Message); + } + [Fact] public async Task HandshakeFailureFromUnknownProtocolSendsResponseWithError() { @@ -958,6 +967,22 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task FailsToInitializeInvalidTypedHub() + { + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(SimpleVoidReturningTypedHub)); + + using (var firstClient = new TestClient()) + { + // ConnectAsync returns a Task and it's the INNER Task that will be faulted. + var connectionTask = await firstClient.ConnectAsync(connectionHandler); + + // We should get a close frame now + var close = Assert.IsType(await firstClient.ReadAsync()); + Assert.Equal("Connection closed with an error.", close.Error); + } + } + [Theory] [MemberData(nameof(HubTypes))] public async Task SendToAllExcept(Type hubType) diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/Internal/TypedClientBuilderTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/Internal/TypedClientBuilderTests.cs new file mode 100644 index 0000000000..2396ffcb12 --- /dev/null +++ b/test/Microsoft.AspNetCore.SignalR.Tests/Internal/TypedClientBuilderTests.cs @@ -0,0 +1,223 @@ +// 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; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Internal; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.SignalR.Tests.Internal +{ + public class TypedClientBuilderTests + { + [Fact] + public async Task ProducesImplementationThatProxiesMethodsToIClientProxyAsync() + { + var clientProxy = new MockProxy(); + var typedProxy = TypedClientBuilder.Build(clientProxy); + + var objArg = new object(); + var task = typedProxy.Method("foo", 42, objArg); + Assert.False(task.IsCompleted); + + Assert.Collection(clientProxy.Sends, + send => + { + Assert.Equal("Method", send.Method); + Assert.Equal("foo", send.Arguments[0]); + Assert.Equal(42, send.Arguments[1]); + Assert.Same(objArg, send.Arguments[2]); + send.Complete(); + }); + + await task.OrTimeout(); + } + + [Fact] + public async Task SupportsSubInterfaces() + { + var clientProxy = new MockProxy(); + var typedProxy = TypedClientBuilder.Build(clientProxy); + + var objArg = new object(); + var task1 = typedProxy.Method("foo", 42, objArg); + Assert.False(task1.IsCompleted); + + var task2 = typedProxy.SubMethod("bar"); + Assert.False(task2.IsCompleted); + + Assert.Collection(clientProxy.Sends, + send1 => + { + Assert.Equal("Method", send1.Method); + Assert.Collection(send1.Arguments, + arg1 => Assert.Equal("foo", arg1), + arg2 => Assert.Equal(42, arg2), + arg3 => Assert.Same(objArg, arg3)); + send1.Complete(); + }, + send2 => + { + Assert.Equal("SubMethod", send2.Method); + Assert.Collection(send2.Arguments, + arg1 => Assert.Equal("bar", arg1)); + send2.Complete(); + }); + + await task1.OrTimeout(); + await task2.OrTimeout(); + } + + [Fact] + public void ThrowsIfProvidedAClass() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal("Type must be an interface.", ex.Message); + } + + [Fact] + public void ThrowsIfProvidedAStruct() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal("Type must be an interface.", ex.Message); + } + + [Fact] + public void ThrowsIfProvidedADelegate() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal("Type must be an interface.", ex.Message); + } + + [Fact] + public void ThrowsIfInterfaceHasVoidReturningMethod() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal($"Cannot generate proxy implementation for '{typeof(IVoidMethodClient).FullName}.{nameof(IVoidMethodClient.Method)}'. All client proxy methods must return '{typeof(Task).FullName}'.", ex.Message); + } + + [Fact] + public void ThrowsIfInterfaceHasNonTaskReturns() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal($"Cannot generate proxy implementation for '{typeof(IStringMethodClient).FullName}.{nameof(IStringMethodClient.Method)}'. All client proxy methods must return '{typeof(Task).FullName}'.", ex.Message); + } + + [Fact] + public void ThrowsIfInterfaceMethodHasOutParam() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal( + $"Cannot generate proxy implementation for '{typeof(IOutParamMethodClient).FullName}.{nameof(IOutParamMethodClient.Method)}'. Client proxy methods must not have 'out' parameters.", ex.Message); + } + + [Fact] + public void ThrowsIfInterfaceMethodHasRefParam() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal( + $"Cannot generate proxy implementation for '{typeof(IRefParamMethodClient).FullName}.{nameof(IRefParamMethodClient.Method)}'. Client proxy methods must not have 'ref' parameters.", ex.Message); + } + + [Fact] + public void ThrowsIfInterfaceHasProperties() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal("Type must not contain properties.", ex.Message); + } + + [Fact] + public void ThrowsIfInterfaceHasEvents() + { + var clientProxy = new MockProxy(); + var ex = Assert.Throws(() => TypedClientBuilder.Build(clientProxy)); + Assert.Equal("Type must not contain events.", ex.Message); + } + + public interface ITestClient + { + Task Method(string arg1, int arg2, object arg3); + } + + public interface IVoidMethodClient + { + void Method(string arg1, int arg2, object arg3); + } + + public interface IStringMethodClient + { + string Method(string arg1, int arg2, object arg3); + } + + public interface IOutParamMethodClient + { + Task Method(out string arg1); + } + + public interface IRefParamMethodClient + { + Task Method(ref string arg1); + } + + public interface IInheritedClient : ITestClient + { + Task SubMethod(string foo); + } + + public interface IPropertiesClient + { + string Property { get; } + } + + public interface IEventsClient + { + event EventHandler Event; + } + + private class MockProxy : IClientProxy + { + public IList Sends { get; } = new List(); + + public Task SendCoreAsync(string method, object[] args) + { + var tcs = new TaskCompletionSource(); + + Sends.Add(new SendContext(method, args, tcs)); + + return tcs.Task; + } + } + + private struct SendContext + { + private TaskCompletionSource _tcs; + + public string Method { get; } + public object[] Arguments { get; } + + public SendContext(string method, object[] arguments, TaskCompletionSource tcs) : this() + { + Method = method; + Arguments = arguments; + _tcs = tcs; + } + + public void Complete() + { + _tcs.TrySetResult(null); + } + } + } +}