Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SignalR] Better integration with TestServer #11888

Open
analogrelay opened this issue Jul 4, 2019 · 21 comments
Open

[SignalR] Better integration with TestServer #11888

analogrelay opened this issue Jul 4, 2019 · 21 comments
Labels
affected-few This issue impacts only small number of customers area-signalr Includes: SignalR clients and servers enhancement This issue represents an ask for new feature or an enhancement to an existing one severity-minor This label is used by an internal tool
Milestone

Comments

@analogrelay
Copy link
Contributor

Right now, it's only possible to use TestServer with the non-WebSockets transports because we only provide a way to replace the HttpMessageHandler. We should consider providing a way to provide a WebSocket "factory" so that we can integrate properly with TestServer.

It might be most appropriate here to add an alternative to WithUrl on HubConnectionBuilder. Consider this possible test code:

var server = new TestServer(webHostBuilder);
var connection = new HubConnectionBuilder()
	.WithTestServer(server, HttpTransportType.WebSockets, o => {
		// Assorted other HttpConnectionOptions
	})
	.Build();

It would be relatively simple to add this API once we have a way to swap out the WebSocket, though we'd probably need to do so in a new assembly to avoid layering issues.

@khteh
Copy link

khteh commented Oct 10, 2019

Bear in mind that WebHostBuilder will be deprecated in future releases.

@analogrelay
Copy link
Contributor Author

Yes, we're aware :). The focus is on better integration with TestServer, not necessarily using WebHostBuilder.

@Suriman
Copy link

Suriman commented Jul 10, 2020

Please, these characteristics are very important to be able to test SignalR with TestServer in integration or unit tests.

@BrennanConroy BrennanConroy added affected-few This issue impacts only small number of customers severity-minor This label is used by an internal tool labels Nov 6, 2020 — with ASP.NET Core Issue Ranking
@suadev
Copy link

suadev commented Feb 8, 2021

👀

@BrennanConroy
Copy link
Member

There is now an API on HttpConnectionOptions to allow replacing the websocket so an extension method on the HubConnectionBuilder can be done now.

@khteh
Copy link

khteh commented May 12, 2021

@BrennanConroy which .Net version? .Net 5 or 6? Thanks.

@BrennanConroy
Copy link
Member

BrennanConroy commented May 12, 2021

6, we don't add or change APIs in already released versions.

@wegylexy
Copy link
Contributor

Not sure if it helps, but here is an example to use named pipe as the underlying transport for testing: https://github.com/wegylexy/SignalRTunnel/blob/0afb58b11f88afcae8962a797f1eace14ce05096/src/Test/Test.cs#L71

@dayadimova
Copy link

Hey @BrennanConroy. I have migrated my integration tests to .NET 6 and I've replaced the WebSocket in HttpConnectionOptions with one created from TestServer. Currently having some issues with authentication - the access token is not sent with the request that establishes a connection to the server. Can you advise on how to add it to either the headers or query string?

SignalR client code:

var connection = new HubConnectionBuilder()
                .WithUrl($"{_baseUrl}/hubs", options =>
                {
                    options.AccessTokenProvider = () => Task.FromResult(token);
                    options.SkipNegotiation = true;
                    options.Transports = HttpTransportType.WebSockets;
                    options.WebSocketFactory = (context, cancellationToken) =>
                    {
                        var webSocketClient = _server.CreateWebSocketClient();
                        var webSocket = webSocketClient.ConnectAsync(context.Uri, cancellationToken).GetAwaiter().GetResult();
                        return ValueTask.FromResult(webSocket);
                    };
                    options.Headers.Add(IntegrationTestConstants.CorrTokenHeaderKey, IntegrationTestConstants.CorrTokenHeaderValue);
                })
                .Build();

@sergey-litvinov
Copy link
Contributor

sergey-litvinov commented Oct 8, 2021

@dayadimova in case of browser's websocket implementation access token can be passed only with uri. And as you are creating WebSocket client manually, you need to add it to your uri as well.

You can check default WebSocketFactory implementation here -

private async ValueTask<WebSocket> DefaultWebSocketFactory(WebSocketConnectionContext context, CancellationToken cancellationToken)

The needed part is at the bottom

            if (_httpConnectionOptions.AccessTokenProvider != null)
            {
                var accessToken = await _httpConnectionOptions.AccessTokenProvider();
                if (!string.IsNullOrWhiteSpace(accessToken))
                {
                    // We can't use request headers in the browser, so instead append the token as a query string in that case
                    if (OperatingSystem.IsBrowser())
                    {
                        var accessTokenEncoded = UrlEncoder.Default.Encode(accessToken);
                        accessTokenEncoded = "access_token=" + accessTokenEncoded;
                        url = Utils.AppendQueryString(url, accessTokenEncoded);
                    }
                    else
                    {
#pragma warning disable CA1416 // Analyzer bug
                        webSocket.Options.SetRequestHeader("Authorization", $"Bearer {accessToken}");
#pragma warning restore CA1416 // Analyzer bug
                    }
                }
            }

so you would need to update your code to something like

var connection = new HubConnectionBuilder()
                .WithUrl($"{_baseUrl}/hubs", options =>
                {
                    options.AccessTokenProvider = () => Task.FromResult(token);
                    options.SkipNegotiation = true;
                    options.Transports = HttpTransportType.WebSockets;
                    options.WebSocketFactory = (context, cancellationToken) =>
                    {
                        var webSocketClient = _server.CreateWebSocketClient();
+                       var url = $"{context.Uri}?access_token={token}";
+                       var webSocket = webSocketClient.ConnectAsync(url, cancellationToken).GetAwaiter().GetResult();
                        return ValueTask.FromResult(webSocket);
                    };
                    options.Headers.Add(IntegrationTestConstants.CorrTokenHeaderKey, IntegrationTestConstants.CorrTokenHeaderValue);
                })
                .Build();

@dayadimova
Copy link

Awesome, thank you for the help, that worked 🙇

@wegylexy
Copy link
Contributor

wegylexy commented Oct 8, 2021

You want to return the connection task instead of blocking a thread.

var connection = new HubConnectionBuilder()
    .WithUrl($"{_baseUrl}/hubs", options =>
    {
        options.AccessTokenProvider = () => Task.FromResult(token);
        options.SkipNegotiation = true;
        options.Transports = HttpTransportType.WebSockets;
        options.WebSocketFactory = (context, cancellationToken) =>
        {
            var webSocketClient = _server.CreateWebSocketClient();
            var url = $"{context.Uri}?access_token={token}";
-           var webSocket = webSocketClient.ConnectAsync(url, cancellationToken).GetAwaiter().GetResult();
+           var webSocketTask = webSocketClient.ConnectAsync(url, cancellationToken);
-           return ValueTask.FromResult(webSocket);
+           return new(webSocketTask);
        };
        options.Headers.Add(IntegrationTestConstants.CorrTokenHeaderKey, IntegrationTestConstants.CorrTokenHeaderValue);
    })
    .Build();

@khteh
Copy link

khteh commented Nov 11, 2021

Where is IntegrationTestConstants defined?

@BrennanConroy
Copy link
Member

I think that's something they defined in their app and unrelated to what is being shown in the sample code.

@khteh
Copy link

khteh commented Nov 11, 2021

The following code snippet works:

            HubConnection connection = new HubConnectionBuilder()
                            .WithUrl("https://localhost/chatHub", o => {
                                o.Transports = HttpTransportType.WebSockets;
                                o.AccessTokenProvider = async () => await AccessTokenProvider();
                                o.SkipNegotiation = true;
                                o.HttpMessageHandlerFactory = _ => _testServer.CreateHandler();
                                o.WebSocketFactory = async (context, cancellationToken) =>
                                {
                                    var wsClient = _testServer.CreateWebSocketClient();
                                    var url = $"{context.Uri}?access_token={await AccessTokenProvider()}";
                                    return await wsClient.ConnectAsync(new Uri(url), cancellationToken);
                                };
                            }).Build();

How to use the AccessTokenProvider option? Is it redundant for this use case?

@deathcat05
Copy link

I seem to be running into an issue with something similar to this. I am using .NET 6, and have created my TestServer and SignalR connection as follows in my integration test function:

await using var application = new WebApplicationFactory<Program>();
            using var client = application.CreateClient();

            string? accessToken = await GetAcessTokenAsync(client, agent.Id, agent.Secret);
            var connection = new HubConnectionBuilder()
                .WithUrl("https://localhost/hub", options =>
                {
                    options.Transports = HttpTransportType.WebSockets;
                    options.AccessTokenProvider = () => Task.FromResult(accessToken);
                    options.SkipNegotiation = true;
                    options.HttpMessageHandlerFactory = _ => application.Server.CreateHandler();
                    options.WebSocketFactory = async (context, cancellationToken) =>
                    {
                        var wsClient = application.Server.CreateWebSocketClient();
                        var url = $"{context.Uri}?access_token={accessToken}";
                        return await wsClient.ConnectAsync(new Uri(url), cancellationToken);
                    };
                })
                .Build();

            await connection.StartAsync();

However, my test fails with the following Error information:

Message: 
System.InvalidOperationException : Incomplete handshake, status code: 401

Stack Trace: 

WebSocketClient.ConnectAsync(Uri uri, CancellationToken cancellationToken)
<<TestShouldReturnTrue>b__3>d.MoveNext() line 42
--- End of stack trace from previous location ---
WebSocketsTransport.StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken)
HttpConnection.StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken)
HttpConnection.SelectAndStartTransport(TransferFormat transferFormat, CancellationToken cancellationToken)
HttpConnection.StartAsyncCore(TransferFormat transferFormat, CancellationToken cancellationToken)
ForceAsyncAwaiter.GetResult()
HttpConnection.StartAsync(TransferFormat transferFormat, CancellationToken cancellationToken)
HttpConnectionFactory.ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
HttpConnectionFactory.ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
HubConnection.StartAsyncCore(CancellationToken cancellationToken)
HubConnection.StartAsyncInner(CancellationToken cancellationToken)
ForceAsyncAwaiter.GetResult()
HubConnection.StartAsync(CancellationToken cancellationToken)
PingPongTests.TestShouldReturnTrue(AgentModel agent) line 47
PingPongTests.TestShouldReturnTrue(AgentModel agent) line 50
--- End of stack trace from previous location ---

My token is issued successfully. I noticed that if I remove the options.SkipNegotiations = true in my code, I get the following exception in my test function:

Message: 

System.AggregateException : Unable to connect to the server with any of the available transports. (WebSockets failed: Incomplete handshake, status code: 401)
---- Microsoft.AspNetCore.Http.Connections.Client.TransportFailedException : WebSockets failed: Incomplete handshake, status code: 401
-------- System.InvalidOperationException : Incomplete handshake, status code: 401

Stack Trace: 

HttpConnection.SelectAndStartTransport(TransferFormat transferFormat, CancellationToken cancellationToken)
HttpConnection.StartAsyncCore(TransferFormat transferFormat, CancellationToken cancellationToken)
ForceAsyncAwaiter.GetResult()
HttpConnection.StartAsync(TransferFormat transferFormat, CancellationToken cancellationToken)
HttpConnectionFactory.ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
HttpConnectionFactory.ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
HubConnection.StartAsyncCore(CancellationToken cancellationToken)
HubConnection.StartAsyncInner(CancellationToken cancellationToken)
ForceAsyncAwaiter.GetResult()
HubConnection.StartAsync(CancellationToken cancellationToken)
PingPongTests.TestShouldReturnTrue(AgentModel agent) line 47
PingPongTests.TestShouldReturnTrue(AgentModel agent) line 50
--- End of stack trace from previous location ---
----- Inner Stack Trace -----
----- Inner Stack Trace -----
WebSocketClient.ConnectAsync(Uri uri, CancellationToken cancellationToken)
<<TestShouldReturnTrue>b__3>d.MoveNext() line 42
--- End of stack trace from previous location ---
WebSocketsTransport.StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken)
HttpConnection.StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken)
HttpConnection.SelectAndStartTransport(TransferFormat transferFormat, CancellationToken cancellationToken)

I have used public partial class Program in Program.cs for the WebApplicationFactory.
Am I missing something important somewhere?

Thanks in advance!

@EddyHaigh
Copy link

Is there anything more clearly defined for testing SignalR with WebApplicationFactory yet? Can we also get some testing documentation in the official docs?

@aleoyakas
Copy link

We're using cookie authentication for our SignalR endpoints which is achieved via a REST endpoint. Currently, there doesn't seem to be a way to share a handler (and therefore the cookies) with TestServer.

Is there plans to include this functionality/is there a workaround that I'm unaware of?

@BrennanConroy
Copy link
Member

It doesn't look like TestServer has first class support for cookies, which means using it with SignalR won't have cookies either.

There is enough extensibility to implement it manually though. If you wrap the HttpMessageHandler from TestServer.CreateHandler you can intercept the Set-Cookie header and add it to a CookieContainer instance that you share with HttpConnectionOptions in SignalR. You'd also need to add cookies from the CookieContainer to requests via the TestServer.CreateHandler overload that has an Action<HttpContext>.

@HakamFostok
Copy link

I am using .NET 7 and in my case, the following code was more than enough

string accessToken = // get it from someplace
builder.WithUrl("https://localhost/hub", o =>
{
    o.AccessTokenProvider = () => Task.FromResult<string?>(accessToken);
    o.HttpMessageHandlerFactory = _ => testServer.CreateHandler();
})

@DomenPigeon
Copy link

Is there are new progress on this issue?

Also just wanted to point out that the StartAsync method takes more than 4s to complete, which is really long to have all tests by default take 4s. I have tested it without the WebApplicationFactory on localhost and it took like 100ms.

var retryPolicy = new RetryPolicy();
var hubConnection = new HubConnectionBuilder()
    .WithUrl("http://localhost/hubPath", opt => opt.HttpMessageHandlerFactory = HttpMessageHandlerFactory)
    .Build();

await hubConnection.StartAsync();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
affected-few This issue impacts only small number of customers area-signalr Includes: SignalR clients and servers enhancement This issue represents an ask for new feature or an enhancement to an existing one severity-minor This label is used by an internal tool
Projects
None yet
Development

No branches or pull requests