File: Internal\TypedClientBuilderTests.cs
Web Access
Project: src\src\SignalR\server\SignalR\test\Microsoft.AspNetCore.SignalR.Tests\Microsoft.AspNetCore.SignalR.Tests.csproj (Microsoft.AspNetCore.SignalR.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.InternalTesting;
 
namespace Microsoft.AspNetCore.SignalR.Tests.Internal;
 
public class TypedClientBuilderTests
{
    [Fact]
    public async Task ProducesImplementationThatProxiesMethodsToIClientProxyAsync()
    {
        var clientProxy = new MockProxy();
        var typedProxy = TypedClientBuilder<ITestClient>.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.Equal(CancellationToken.None, send.CancellationToken);
                Assert.Same(objArg, send.Arguments[2]);
                send.Complete();
            });
 
        await task.DefaultTimeout();
    }
 
    [Fact]
    public async Task ProducesImplementationThatProxiesMethodsToIRenamedClientProxyAsync()
    {
        var clientProxy = new MockProxy();
        var typedProxy = TypedClientBuilder<IRenamedTestClient>.Build(clientProxy);
 
        var objArg = new object();
        var task = typedProxy.MethodAsync("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.Equal(CancellationToken.None, send.CancellationToken);
                Assert.Same(objArg, send.Arguments[2]);
                send.Complete();
            });
 
        await task.DefaultTimeout();
    }
 
    [Fact]
    public async Task SupportsSubInterfaces()
    {
        var clientProxy = new MockProxy();
        var typedProxy = TypedClientBuilder<IInheritedClient>.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));
                Assert.Equal(CancellationToken.None, send1.CancellationToken);
                send1.Complete();
            },
            send2 =>
            {
                Assert.Equal("SubMethod", send2.Method);
                Assert.Collection(send2.Arguments,
                    arg1 => Assert.Equal("bar", arg1));
                Assert.Equal(CancellationToken.None, send2.CancellationToken);
                send2.Complete();
            });
 
        await task1.DefaultTimeout();
        await task2.DefaultTimeout();
    }
 
    [Fact]
    public async Task SupportsCancellationToken()
    {
        var clientProxy = new MockProxy();
        var typedProxy = TypedClientBuilder<ICancellationTokenMethod>.Build(clientProxy);
        CancellationTokenSource cts1 = new CancellationTokenSource();
        var task1 = typedProxy.Method("foo", cts1.Token);
        Assert.False(task1.IsCompleted);
 
        CancellationTokenSource cts2 = new CancellationTokenSource();
        var task2 = typedProxy.NoArgumentMethod(cts2.Token);
        Assert.False(task2.IsCompleted);
 
        Assert.Collection(clientProxy.Sends,
            send1 =>
            {
                Assert.Equal("Method", send1.Method);
                Assert.Single(send1.Arguments);
                Assert.Collection(send1.Arguments,
                    arg1 => Assert.Equal("foo", arg1));
                Assert.Equal(cts1.Token, send1.CancellationToken);
                send1.Complete();
            },
            send2 =>
            {
                Assert.Equal("NoArgumentMethod", send2.Method);
                Assert.Empty(send2.Arguments);
                Assert.Equal(cts2.Token, send2.CancellationToken);
                send2.Complete();
            });
 
        await task1.DefaultTimeout();
        await task2.DefaultTimeout();
    }
 
    [Fact]
    public void ThrowsIfProvidedAClass()
    {
        var clientProxy = new MockProxy();
        var ex = Assert.Throws<InvalidOperationException>(() => TypedClientBuilder<object>.Build(clientProxy));
        Assert.Equal("Type must be an interface.", ex.Message);
    }
 
    [Fact]
    public void ThrowsIfProvidedAStruct()
    {
        var clientProxy = new MockProxy();
        var ex = Assert.Throws<InvalidOperationException>(() => TypedClientBuilder<ValueTask>.Build(clientProxy));
        Assert.Equal("Type must be an interface.", ex.Message);
    }
 
    [Fact]
    public void ThrowsIfProvidedADelegate()
    {
        var clientProxy = new MockProxy();
        var ex = Assert.Throws<InvalidOperationException>(() => TypedClientBuilder<EventHandler>.Build(clientProxy));
        Assert.Equal("Type must be an interface.", ex.Message);
    }
 
    [Fact]
    public void ThrowsIfInterfaceHasVoidReturningMethod()
    {
        var clientProxy = new MockProxy();
        var ex = Assert.Throws<InvalidOperationException>(() => TypedClientBuilder<IVoidMethodClient>.Build(clientProxy));
        Assert.Equal($"Cannot generate proxy implementation for '{typeof(IVoidMethodClient).FullName}.{nameof(IVoidMethodClient.Method)}'. All client proxy methods must return '{typeof(Task).FullName}' or '{typeof(Task).FullName}<T>'.", ex.Message);
    }
 
    [Fact]
    public void ThrowsIfInterfaceHasNonTaskReturns()
    {
        var clientProxy = new MockProxy();
        var ex = Assert.Throws<InvalidOperationException>(() => TypedClientBuilder<IStringMethodClient>.Build(clientProxy));
        Assert.Equal($"Cannot generate proxy implementation for '{typeof(IStringMethodClient).FullName}.{nameof(IStringMethodClient.Method)}'. All client proxy methods must return '{typeof(Task).FullName}' or '{typeof(Task).FullName}<T>'.", ex.Message);
    }
 
    [Fact]
    public void ThrowsIfInterfaceMethodHasOutParam()
    {
        var clientProxy = new MockProxy();
        var ex = Assert.Throws<InvalidOperationException>(() => TypedClientBuilder<IOutParamMethodClient>.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<InvalidOperationException>(() => TypedClientBuilder<IRefParamMethodClient>.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<InvalidOperationException>(() => TypedClientBuilder<IPropertiesClient>.Build(clientProxy));
        Assert.Equal("Type must not contain properties.", ex.Message);
    }
 
    [Fact]
    public void ThrowsIfInterfaceHasEvents()
    {
        var clientProxy = new MockProxy();
        var ex = Assert.Throws<InvalidOperationException>(() => TypedClientBuilder<IEventsClient>.Build(clientProxy));
        Assert.Equal("Type must not contain events.", ex.Message);
    }
 
    [Fact]
    public async Task ProducesImplementationThatProxiesMethodsToISingleClientProxyAsync()
    {
        var clientProxy = new MockSingleClientProxy();
        var typedProxy = TypedClientBuilder<ITestClient>.Build(clientProxy);
 
        var objArg = new object();
        var task = typedProxy.GetValue(1008, objArg, "test");
        Assert.False(task.IsCompleted);
 
        Assert.Collection(clientProxy.Sends,
            send =>
            {
                Assert.Equal("GetValue", send.Method);
                Assert.Collection(send.Arguments,
                    arg1 => Assert.Equal(1008, arg1),
                    arg2 => Assert.Same(objArg, arg2),
                    arg3 => Assert.Same("test", arg3));
                Assert.Equal(CancellationToken.None, send.CancellationToken);
                send.Complete();
            });
 
        var result = await task.DefaultTimeout();
        Assert.Equal(default(int), result);
    }
 
    [Fact]
    public async Task ThrowsIfReturnMethodUsedWithoutSingleClientProxy()
    {
        var clientProxy = new MockProxy();
        var typedProxy = TypedClientBuilder<ITestClient>.Build(clientProxy);
 
        var objArg = new object();
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => typedProxy.GetValue(102, objArg, "test")).DefaultTimeout();
        Assert.Equal("InvokeAsync only works with Single clients.", ex.Message);
 
        Assert.Empty(clientProxy.Sends);
    }
 
    [Fact]
    public async Task ResultMethodSupportsCancellationToken()
    {
        var clientProxy = new MockSingleClientProxy();
        var typedProxy = TypedClientBuilder<ICancellationTokenMethod>.Build(clientProxy);
        CancellationTokenSource cts1 = new CancellationTokenSource();
        var task1 = typedProxy.MethodReturning("foo", cts1.Token);
        Assert.False(task1.IsCompleted);
 
        CancellationTokenSource cts2 = new CancellationTokenSource();
        var task2 = typedProxy.NoArgumentMethodReturning(cts2.Token);
        Assert.False(task2.IsCompleted);
 
        Assert.Collection(clientProxy.Sends,
            send1 =>
            {
                Assert.Equal("MethodReturning", send1.Method);
                Assert.Single(send1.Arguments);
                Assert.Collection(send1.Arguments,
                    arg1 => Assert.Equal("foo", arg1));
                Assert.Equal(cts1.Token, send1.CancellationToken);
                send1.Complete();
            },
            send2 =>
            {
                Assert.Equal("NoArgumentMethodReturning", send2.Method);
                Assert.Empty(send2.Arguments);
                Assert.Equal(cts2.Token, send2.CancellationToken);
                send2.Complete();
            });
 
        var result = await task1.DefaultTimeout();
        Assert.Equal(default(string), result);
        var result2 = await task2.DefaultTimeout();
        Assert.Equal(default(int), result2);
    }
 
    public interface ITestClient
    {
        Task Method(string arg1, int arg2, object arg3);
        Task<int> GetValue(int arg1, object arg2, string arg3);
    }
 
    public interface IRenamedTestClient
    {
        [HubMethodName("Method")]
        Task MethodAsync(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 ICancellationTokenMethod
    {
        Task Method(string foo, CancellationToken cancellationToken);
        Task NoArgumentMethod(CancellationToken cancellationToken);
 
        Task<int> NoArgumentMethodReturning(CancellationToken cancellationToken);
        Task<string> MethodReturning(string foo, CancellationToken cancellationToken);
    }
 
    public interface IPropertiesClient
    {
        string Property { get; }
    }
 
    public interface IEventsClient
    {
        event EventHandler Event;
    }
 
    public interface ITestReturnValueClient
    {
        Task<int> GetValue();
    }
 
    private class MockProxy : IClientProxy
    {
        public IList<SendContext> Sends { get; } = new List<SendContext>();
 
        public Task SendCoreAsync(string method, object[] args, CancellationToken cancellationToken)
        {
            var tcs = new TaskCompletionSource();
 
            Sends.Add(new SendContext(method, args, cancellationToken, tcs));
 
            return tcs.Task;
        }
    }
 
    private class MockSingleClientProxy : ISingleClientProxy
    {
        public IList<SendContext> Sends { get; } = new List<SendContext>();
 
        public Task SendCoreAsync(string method, object[] args, CancellationToken cancellationToken)
        {
            var tcs = new TaskCompletionSource();
 
            Sends.Add(new SendContext(method, args, cancellationToken, tcs));
 
            return tcs.Task;
        }
 
        public async Task<T> InvokeCoreAsync<T>(string method, object[] args, CancellationToken cancellationToken = default)
        {
            var tcs = new TaskCompletionSource();
 
            Sends.Add(new SendContext(method, args, cancellationToken, tcs));
 
            await tcs.Task;
            return default(T);
        }
    }
 
    private struct SendContext
    {
        private readonly TaskCompletionSource _tcs;
 
        public string Method { get; }
        public object[] Arguments { get; }
        public CancellationToken CancellationToken { get; }
 
        public SendContext(string method, object[] arguments, CancellationToken cancellationToken, TaskCompletionSource tcs) : this()
        {
            Method = method;
            Arguments = arguments;
            CancellationToken = cancellationToken;
            _tcs = tcs;
        }
 
        public void Complete()
        {
            _tcs.TrySetResult();
        }
    }
}