From 0a4114603ea5b26dd2b39660f1ec83aee79c6b74 Mon Sep 17 00:00:00 2001 From: Terence Fan Date: Wed, 5 Feb 2025 18:03:54 +0800 Subject: [PATCH] connection reroute test cases --- .editorconfig | 3 +- .../DefaultHeaderProvider.cs | 11 + .../Interfaces/ICustomHeaderProvider.cs | 11 + .../Common/ITestHubConnection.cs | 29 +++ .../Common/ITestHubConnectionGroup.cs | 10 + .../Common/ITestServer.cs | 15 ++ .../Common/IngressHeaderProvider.cs | 29 +++ .../Common/TestHubConnectionFactory.cs | 195 +++++++++++++++++ .../Common/WebSocketProxy.cs | 77 +++++++ .../DefaultHubProtocolResolver.cs | 70 ++++++ .../Management/NegotiateProcessorE2EFacts.cs | 23 +- .../Management/ServiceManagerE2EFacts.cs | 2 + .../Microsoft.Azure.SignalR.E2ETests.csproj | 20 +- .../ServerSDK/CrossServerRerouteTests.cs | 58 +++++ .../ServerSDK/SameServerRerouteTests.cs | 206 ++++++++++++++++++ .../SignalR/SignalRServiceE2EFacts.cs | 34 --- .../SignalR/TestClientSet.cs | 90 -------- .../SignalR/TestClientSetFactory.cs | 16 -- .../SignalR/TestHub.cs | 96 -------- .../SignalR/TestServer.cs | 53 ----- .../SignalR/TestServerFactory.cs | 16 -- .../SignalR/TestStartup.cs | 52 ----- .../TestHub.cs | 14 ++ .../TestServer.cs | 96 ++++++++ .../TestStartup.cs | 53 +++++ .../MockServiceConnectionFactory.cs | 50 ++--- .../MockServiceHubDispatcher.cs | 120 +++++----- ...ipIfConnectionStringNotPresentAttribute.cs | 1 + .../TestConfiguration.cs | 1 + .../ServiceConnectionTests.cs | 2 + test/appsettings.Test.json | 11 +- 31 files changed, 984 insertions(+), 480 deletions(-) create mode 100644 src/Microsoft.Azure.SignalR.Common/DefaultHeaderProvider.cs create mode 100644 src/Microsoft.Azure.SignalR.Common/Interfaces/ICustomHeaderProvider.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/Common/ITestHubConnection.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/Common/ITestHubConnectionGroup.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/Common/ITestServer.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/Common/IngressHeaderProvider.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/Common/TestHubConnectionFactory.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/Common/WebSocketProxy.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/DefaultHubProtocolResolver.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/ServerSDK/CrossServerRerouteTests.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/ServerSDK/SameServerRerouteTests.cs delete mode 100644 test/Microsoft.Azure.SignalR.E2ETests/SignalR/SignalRServiceE2EFacts.cs delete mode 100644 test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestClientSet.cs delete mode 100644 test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestClientSetFactory.cs delete mode 100644 test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestHub.cs delete mode 100644 test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestServer.cs delete mode 100644 test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestServerFactory.cs delete mode 100644 test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestStartup.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/TestHub.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/TestServer.cs create mode 100644 test/Microsoft.Azure.SignalR.E2ETests/TestStartup.cs diff --git a/.editorconfig b/.editorconfig index a84ec13e0..b07af16a2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -51,7 +51,7 @@ csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true # Namespace settings -csharp_style_namespace_declarations = file_scoped:silent +csharp_style_namespace_declarations = file_scoped:error # Brace settings csharp_prefer_braces = true:silent# Prefer curly braces even for one line of code @@ -102,6 +102,7 @@ csharp_prefer_simple_default_expression = true:suggestion csharp_style_prefer_local_over_anonymous_function = true:suggestion csharp_style_prefer_index_operator = true:suggestion csharp_style_prefer_range_operator = true:suggestion +csharp_space_around_binary_operators = before_and_after [*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}] indent_size = 2 diff --git a/src/Microsoft.Azure.SignalR.Common/DefaultHeaderProvider.cs b/src/Microsoft.Azure.SignalR.Common/DefaultHeaderProvider.cs new file mode 100644 index 000000000..388df51ff --- /dev/null +++ b/src/Microsoft.Azure.SignalR.Common/DefaultHeaderProvider.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.Azure.SignalR; + +internal class DefaultHeaderProvider : ICustomHeaderProvider +{ + public IEnumerable<(string key, string val)> Headers => []; +} diff --git a/src/Microsoft.Azure.SignalR.Common/Interfaces/ICustomHeaderProvider.cs b/src/Microsoft.Azure.SignalR.Common/Interfaces/ICustomHeaderProvider.cs new file mode 100644 index 000000000..f5ca0abfa --- /dev/null +++ b/src/Microsoft.Azure.SignalR.Common/Interfaces/ICustomHeaderProvider.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.Azure.SignalR; + +internal interface ICustomHeaderProvider +{ + IEnumerable<(string key, string val)> Headers { get; } +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestHubConnection.cs b/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestHubConnection.cs new file mode 100644 index 000000000..ad0b233f0 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestHubConnection.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; + +namespace Microsoft.Azure.SignalR.E2ETests.Common; + +public interface ITestHubConnection +{ + public string ConnectionId { get; } + + public string User { get; } + + public int MessageCount { get; } + + public void Listen(params string[] methods); + + public Task StartAsync(); + + public Task StopAsync(); + + public Task SendAsync(string method, params string[] messages); + + public void ResetInvoke(string method); + + public Task ExpectInvokeAsync(string method, params string[] message); + + public void ResetMessageCount(); +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestHubConnectionGroup.cs b/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestHubConnectionGroup.cs new file mode 100644 index 000000000..6c8328eb3 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestHubConnectionGroup.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.Azure.SignalR.E2ETests.Common; + +public interface ITestHubConnectionGroup : ITestHubConnection, IEnumerable +{ +} \ No newline at end of file diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestServer.cs b/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestServer.cs new file mode 100644 index 000000000..7dd9617a6 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/Common/ITestServer.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Azure.SignalR.E2ETests.Common; + +public interface ITestServer : IDisposable +{ + Task StartAsync(Dictionary? configuration = null); + + Task StopAsync(); +} \ No newline at end of file diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Common/IngressHeaderProvider.cs b/test/Microsoft.Azure.SignalR.E2ETests/Common/IngressHeaderProvider.cs new file mode 100644 index 000000000..8b6514c40 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/Common/IngressHeaderProvider.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Azure.SignalR.E2ETests.Common; + +internal sealed class IngressHeaderProvider : ICustomHeaderProvider +{ + public const string AsrsInternalForwardedBy = "X-ASRS-Internal-Forwarded-By"; + + public const string PodA = "podA"; + + public const string PodB = "podB"; + + private static readonly string[] Ingresses = [PodA, PodB]; + + private static readonly Random Random = new(); + + public IEnumerable<(string key, string val)> Headers + { + get + { + var podName = Ingresses[Random.Next() % Ingresses.Length]; + yield return (AsrsInternalForwardedBy, podName); + } + } +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Common/TestHubConnectionFactory.cs b/test/Microsoft.Azure.SignalR.E2ETests/Common/TestHubConnectionFactory.cs new file mode 100644 index 000000000..553772cd5 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/Common/TestHubConnectionFactory.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.SignalR.Client; + +using Xunit; + +namespace Microsoft.Azure.SignalR.E2ETests.Common; + +public class TestHubConnectionFactory +{ + public bool EnableStatefulReconnect { get; set; } + + public ITestHubConnection NewConnection(string host, string? hub = null, string userId = "foo") + { + return new TestHubConnection(host, hub) + { + User = userId, + EnableStatefulReconnect = EnableStatefulReconnect + }; + } + + public ITestHubConnectionGroup NewConnectionGroup(string host, int count, string? hub = null, string userId = "foo") + { + return new TestHubConnectionGroup(host, count, hub) + { + User = userId, + EnableStatefulReconnect = EnableStatefulReconnect + }; + } + + private sealed class TestHubConnection(string host, string? hub = null) : ITestHubConnection + { + private readonly ConcurrentDictionary> _expectedInvokes = new(); + + private HubConnection? _hubConnection; + + private volatile int _messageCount; + + public string User { get; init; } = "foo"; + + public int MessageCount => _messageCount; + + public bool EnableStatefulReconnect { get; init; } + + public string ConnectionId => _hubConnection?.ConnectionId ?? throw NotReady; + + private static Exception NotReady { get; } = new InvalidOperationException("HubConnection is not in connected state."); + + public Task StartAsync() + { + BuildConnectionIfNull(); + return _hubConnection.StartAsync(); + } + + public Task StopAsync() => _hubConnection?.StopAsync() ?? Task.CompletedTask; + + public Task SendAsync(string method, params string[] messages) + { + if (_hubConnection == null || _hubConnection.State != HubConnectionState.Connected) + { + throw NotReady; + } + return _hubConnection.SendCoreAsync(method, messages); + } + + public void Listen(params string[] methods) + { + BuildConnectionIfNull(); + foreach (var method in methods) + { + _expectedInvokes.TryAdd(method, new TaskCompletionSource()); + _hubConnection.On(method, (Action)(message => Invoke(method, message))); + } + } + + public void ResetInvoke(string method) + { + _expectedInvokes.AddOrUpdate(method, + new TaskCompletionSource(), + (method, ov) => ov.Task.IsCompleted ? new TaskCompletionSource() : ov); + } + + public async Task ExpectInvokeAsync(string method, params string[] messages) + { + Assert.Equal(messages, await _expectedInvokes[method].Task); + } + + public void ResetMessageCount() + { + _messageCount = 0; + } + + private void Invoke(string method, params string[] messages) + { + Interlocked.Increment(ref _messageCount); + if (_expectedInvokes.TryGetValue(method, out var source)) + { + source.TrySetResult(messages); + } + } + + [MemberNotNull(nameof(_hubConnection))] + private void BuildConnectionIfNull() + { + if (_hubConnection == null) + { + hub ??= nameof(TestHub); + var url = $"{host}/{hub}?user={User}"; + var builder = new HubConnectionBuilder().WithUrl(url); + + if (EnableStatefulReconnect) + { + builder = builder.WithStatefulReconnect(); + } + _hubConnection = builder.Build(); + } + } + } + + private sealed class TestHubConnectionGroup(string host, int count, string? hub = null) : ITestHubConnectionGroup + { + private List? _connections; + + public bool EnableStatefulReconnect { get; init; } + + public IEnumerable Connections + { + get + { + _connections ??= (from i in Enumerable.Range(0, count) + select new TestHubConnection(host, hub) + { + User = User, + EnableStatefulReconnect = EnableStatefulReconnect, + } as ITestHubConnection).ToList(); + return _connections; + } + } + + public string User { get; init; } = string.Empty; + + public int MessageCount => Connections.Select(x => x.MessageCount).Sum(); + + public string ConnectionId => throw new NotImplementedException("Connection group does not have ConnectionId."); + + public void Listen(params string[] methods) + { + foreach (var connection in Connections) + { + connection.Listen(methods); + } + } + + public async Task StartAsync() => await Task.WhenAll(Connections.Select(x => x.StartAsync())); + + public IEnumerator GetEnumerator() => Connections.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public async Task StopAsync() => await Task.WhenAll(Connections.Select(x => x.StopAsync())); + + public void ResetInvoke(string method) + { + foreach (var connection in Connections) + { + connection.ResetInvoke(method); + } + } + + public Task ExpectInvokeAsync(string method, params string[] messages) + { + return Task.WhenAll(Connections.Select(x => x.ExpectInvokeAsync(method, messages))); + } + + public Task SendAsync(string method, params string[] messages) => Task.WhenAll(Connections.Select(x => x.SendAsync(method, messages))); + + public void ResetMessageCount() + { + foreach (var connection in Connections) + { + connection.ResetMessageCount(); + } + } + } +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Common/WebSocketProxy.cs b/test/Microsoft.Azure.SignalR.E2ETests/Common/WebSocketProxy.cs new file mode 100644 index 000000000..bb1333dee --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/Common/WebSocketProxy.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Azure.SignalR.E2ETests.Common; + +public class WebSocketProxy(string targetUri) +{ + public async Task HandleWebSocket(HttpContext context) + { + if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = 400; + return; + } + + using var clientSocket = await context.WebSockets.AcceptWebSocketAsync(); + using var serverSocket = new ClientWebSocket(); + + await serverSocket.ConnectAsync(new Uri(targetUri), CancellationToken.None); + + var clientToServer = RelayMessages(clientSocket, serverSocket); + var serverToClient = RelayMessages(serverSocket, clientSocket); + + await Task.WhenAny(clientToServer, serverToClient); + } + + public void Start(int listenPort) + { + Host.CreateDefaultBuilder([]) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseKestrel(options => options.ListenAnyIP(listenPort)); + webBuilder.Configure(app => + { + app.UseWebSockets(); + app.Run(HandleWebSocket); + }); + }) + .Build() + .Run(); + } + + private static async Task RelayMessages(WebSocket source, WebSocket destination) + { + var buffer = new byte[4096]; + + try + { + while (source.State == WebSocketState.Open && destination.State == WebSocketState.Open) + { + var result = await source.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Close) + { + await destination.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + break; + } + + await destination.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + } + } + catch (Exception ex) + { + Console.WriteLine($"WebSocket Error: {ex.Message}"); + } + } +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/DefaultHubProtocolResolver.cs b/test/Microsoft.Azure.SignalR.E2ETests/DefaultHubProtocolResolver.cs new file mode 100644 index 000000000..8f5104c32 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/DefaultHubProtocolResolver.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.Azure.SignalR.E2ETests; + +// copied from https://github.com/aspnet/AspNetCore/blob/release/3.0-preview7/src/SignalR/server/Core/src/Internal/DefaultHubProtocolResolver.cs +internal sealed class DefaultHubProtocolResolver : IHubProtocolResolver +{ + private readonly ILogger _logger; + + private readonly List _hubProtocols; + + private readonly Dictionary _availableProtocols; + + public IReadOnlyList AllProtocols => _hubProtocols; + + public DefaultHubProtocolResolver(IEnumerable availableProtocols, ILogger logger) + { + _logger = logger ?? NullLogger.Instance; + _availableProtocols = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // We might get duplicates in _hubProtocols, but we're going to check it and overwrite in just a sec. + _hubProtocols = availableProtocols.ToList(); + foreach (var protocol in _hubProtocols) + { + Log.RegisteredSignalRProtocol(_logger, protocol.Name, protocol.GetType()); + _availableProtocols[protocol.Name] = protocol; + } + } + + public IHubProtocol GetProtocol(string protocolName, IReadOnlyList? supportedProtocols) + { + protocolName = protocolName ?? throw new ArgumentNullException(nameof(protocolName)); + + if (_availableProtocols.TryGetValue(protocolName, out var protocol) && (supportedProtocols == null || supportedProtocols.Contains(protocolName, StringComparer.OrdinalIgnoreCase))) + { + Log.FoundImplementationForProtocol(_logger, protocolName); + return protocol; + } + throw new NotSupportedException(protocolName); + } + + private static class Log + { + private static readonly Action Registered = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RegisteredSignalRProtocol"), "Registered SignalR Protocol: {ProtocolName}, implemented by {ImplementationType}."); + + private static readonly Action Found = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "FoundImplementationForProtocol"), "Found protocol implementation for requested protocol: {ProtocolName}."); + + public static void RegisteredSignalRProtocol(ILogger logger, string protocolName, Type implementationType) + { + Registered(logger, protocolName, implementationType, null); + } + + public static void FoundImplementationForProtocol(ILogger logger, string protocolName) + { + Found(logger, protocolName, null); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Management/NegotiateProcessorE2EFacts.cs b/test/Microsoft.Azure.SignalR.E2ETests/Management/NegotiateProcessorE2EFacts.cs index b5990710f..0bd0e11f5 100644 --- a/test/Microsoft.Azure.SignalR.E2ETests/Management/NegotiateProcessorE2EFacts.cs +++ b/test/Microsoft.Azure.SignalR.E2ETests/Management/NegotiateProcessorE2EFacts.cs @@ -4,31 +4,30 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Azure.SignalR; +using Microsoft.Azure.SignalR.Management; using Microsoft.Azure.SignalR.Tests.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; + using Xunit; using Xunit.Abstractions; -namespace Microsoft.Azure.SignalR.Management.E2ETests +namespace Microsoft.Azure.SignalR.E2ETests.Management { - public class NegotiateProcessorE2EFacts + public class NegotiateProcessorE2EFacts(ITestOutputHelper outputHelper) { - private readonly ITestOutputHelper _outputHelper; - - public NegotiateProcessorE2EFacts(ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - } + private readonly ITestOutputHelper _outputHelper = outputHelper; [ConditionalFact] [SkipIfConnectionStringNotPresent] public async Task ColdStartNegotiateTest() { var hubName = "hub"; - ServiceCollection services = new ServiceCollection(); + var services = new ServiceCollection(); services.AddSignalRServiceManager(); //configure two fake service endpoints and one real endpoints. @@ -40,15 +39,15 @@ public async Task ColdStartNegotiateTest() }); //enable test output - services.AddSingleton(new LoggerFactory(new List { new XunitLoggerProvider(_outputHelper) })).AddSingleton>(services.ToList()); + services.AddSingleton(new LoggerFactory([new XunitLoggerProvider(_outputHelper)])).AddSingleton>([.. services]); var manager = services.BuildServiceProvider().GetRequiredService(); var hubContext = await manager.CreateHubContextAsync(hubName); var realEndpoint = new ServiceEndpoint(TestConfiguration.Instance.ConnectionString).Endpoint; //reduce the effect of randomness - for (int i = 0; i < 5; i++) + for (var i = 0; i < 5; i++) { - var clientEndoint = await (hubContext as ServiceHubContext).NegotiateAsync(); + var clientEndoint = await (hubContext as ServiceHubContext)!.NegotiateAsync(); var expectedUrl = ClientEndpointUtils.GetExpectedClientEndpoint(hubName, null, realEndpoint); Assert.Equal(expectedUrl, clientEndoint.Url); } diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Management/ServiceManagerE2EFacts.cs b/test/Microsoft.Azure.SignalR.E2ETests/Management/ServiceManagerE2EFacts.cs index eb52e8dac..0b71edb4f 100644 --- a/test/Microsoft.Azure.SignalR.E2ETests/Management/ServiceManagerE2EFacts.cs +++ b/test/Microsoft.Azure.SignalR.E2ETests/Management/ServiceManagerE2EFacts.cs @@ -2,9 +2,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Threading.Tasks; + using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Azure.SignalR.Management; using Microsoft.Azure.SignalR.Tests.Common; + using Xunit; namespace Microsoft.Azure.SignalR.E2ETests.Management diff --git a/test/Microsoft.Azure.SignalR.E2ETests/Microsoft.Azure.SignalR.E2ETests.csproj b/test/Microsoft.Azure.SignalR.E2ETests/Microsoft.Azure.SignalR.E2ETests.csproj index 0930c7007..5c611deb2 100644 --- a/test/Microsoft.Azure.SignalR.E2ETests/Microsoft.Azure.SignalR.E2ETests.csproj +++ b/test/Microsoft.Azure.SignalR.E2ETests/Microsoft.Azure.SignalR.E2ETests.csproj @@ -1,30 +1,26 @@ - + net8.0 false + enable + + + - - - - - - - + - - - - + + diff --git a/test/Microsoft.Azure.SignalR.E2ETests/ServerSDK/CrossServerRerouteTests.cs b/test/Microsoft.Azure.SignalR.E2ETests/ServerSDK/CrossServerRerouteTests.cs new file mode 100644 index 000000000..deb4b22c7 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/ServerSDK/CrossServerRerouteTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Azure.SignalR.E2ETests.Common; +using Microsoft.Azure.SignalR.Tests.Common; + +using Xunit.Abstractions; + +namespace Microsoft.Azure.SignalR.E2ETests.ServerSDK; + +[SkipIfConnectionStringNotPresent] +public sealed class CrossServerRerouteTests(ITestOutputHelper output) : VerifiableLoggedTest(output) +{ + private readonly TestHubConnectionFactory _hubConnectionFactory = new(); + + private readonly ITestOutputHelper _output = output; + + [ConditionalFact] + public async void TestMigrateClients() + { + using var server1 = new TestServer(_output); + using var server2 = new TestServer(_output); + using var server3 = new TestServer(_output); + + var host1 = await server1.StartAsync(); + var host2 = server2.StartAsync(); + + var method = nameof(TestHub.Echo); + + var connections = _hubConnectionFactory.NewConnectionGroup(host1, 3); + connections.Listen(method); + await connections.StartAsync(); + + await connections.SendAsync(method, "ping"); + await connections.ExpectInvokeAsync(method, "ping"); + + #region connections should be migrated to server2 + await server2.StartAsync(); + await server1.StopAsync(); + + connections.ResetInvoke(method); + await connections.SendAsync(method, "pong"); + await connections.ExpectInvokeAsync(method, "pong"); + #endregion connections should be migrated to server2 + + #region connections should be migrated to server3 + await server3.StartAsync(); + await server2.StopAsync(); + + connections.ResetInvoke(method); + await connections.SendAsync(method, "pang"); + await connections.ExpectInvokeAsync(method, "pang"); + #endregion connections should be migrated to server3 + + await server3.StopAsync(); + } +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/ServerSDK/SameServerRerouteTests.cs b/test/Microsoft.Azure.SignalR.E2ETests/ServerSDK/SameServerRerouteTests.cs new file mode 100644 index 000000000..caafc9022 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/ServerSDK/SameServerRerouteTests.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Connections.Client; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Azure.SignalR; +using Microsoft.Azure.SignalR.E2ETests.Common; +using Microsoft.Azure.SignalR.Protocol; +using Microsoft.Azure.SignalR.Tests.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Newtonsoft.Json; + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Azure.SignalR.E2ETests.ServerSDK; + +[SkipIfConnectionStringNotPresent] +public class SameServerRerouteTests(ITestOutputHelper output) : VerifiableLoggedTest(output) +{ + private static int ServerCount; + + private static int ClientCount; + + private readonly TestHubConnectionFactory _hubConnectionFactory = new(); + + private readonly ITestOutputHelper _output = output; + + [Fact] + public async Task TestIngressReload() + { + using var server = new TestServer(_output); + + var host = await server.StartAsync(); + + const string method = nameof(TestHub.Echo); + const int ConnectionCount = 10; + const int MessagePerConnection = 1000; + + // wait server connection connected. + await Task.Delay(1000); + var connections = _hubConnectionFactory.NewConnectionGroup(host, ConnectionCount); + connections.Listen(method); + await connections.StartAsync(); + + _ = TriggerIngressReloadAfter(IngressHeaderProvider.PodA, 5000); + + for (var i = 0; i < MessagePerConnection; i++) + { + await connections.SendAsync(method, "foo"); + await Task.Delay(10); + } + + Assert.Equal(ConnectionCount * MessagePerConnection, connections.MessageCount); + + await server.StopAsync(); + } + + [ConditionalFact] + public async Task Test() + { + const string hub = "foo"; + + using var provider = StartVerifiableLog(out var loggerFactory); + + var nameProvider = new DefaultServerNameProvider(); + var logger = loggerFactory.CreateLogger(); + var hubProtocolResolver = new DefaultHubProtocolResolver([new JsonHubProtocol()], logger); + + var handler = new EndlessConnectionHandler(loggerFactory); + ConnectionDelegate connectionDelegate = handler.OnConnectedAsync; + + var factory = new ServiceConnectionFactory(new ServiceProtocol(), + new ClientConnectionManager(), + new ConnectionFactory(nameProvider, loggerFactory), + loggerFactory, + connectionDelegate, + new ClientConnectionFactory(loggerFactory), + nameProvider, + new DefaultServiceEventHandler(loggerFactory), + new DummyClientInvocationManager(), + new DefaultHeaderProvider(), + hubProtocolResolver); + + var serviceEndpoint = new ServiceEndpoint(TestConfiguration.Instance.ConnectionString); + var serviceEndpointProvider = new ServiceEndpointProvider(serviceEndpoint, new ServiceOptions()); + var hubServiceEndpoint = new HubServiceEndpoint(hub, serviceEndpointProvider, serviceEndpoint); + + var messageHandler = new StrongServiceConnectionContainer(factory, 1, 1, hubServiceEndpoint, logger); + var ackHandler = new AckHandler(); + + var connection1 = factory.Create(hubServiceEndpoint, messageHandler, ackHandler, ServiceConnectionType.Default); + var connection2 = factory.Create(hubServiceEndpoint, messageHandler, ackHandler, ServiceConnectionType.Default); + + var serverConnectionTask1 = connection1.StartAsync(); + var serverConnectionTask2 = connection2.StartAsync(); + + await Task.Delay(1000); + + var connectionString = TestConfiguration.Instance.ConnectionString; + var audience = $"http://localhost/client/?hub={hub}"; + var clientPath = $"http://localhost:8080/client/?hub={hub}"; + var accessKey = new AccessKey(ConnectionStringParser.Parse(connectionString).AccessKey); + var accessToken = await accessKey.GenerateAccessTokenAsync(audience, [], TimeSpan.FromMinutes(1), AccessTokenAlgorithm.HS256); + + var connectionOptions = new HttpConnectionOptions(); + connectionOptions.Headers.Add("Authorization", $"Bearer {accessToken}"); + var connectionFactory = new HttpConnectionFactory(Options.Create(connectionOptions), loggerFactory); + var endpoint = new UriEndPoint(new Uri(clientPath)); + + var hubConnection = new HubConnection(connectionFactory, new JsonHubProtocol(), endpoint, handler.ServiceProvider, loggerFactory); + hubConnection.On("bar", (int x) => + { + Assert.Equal(Interlocked.Increment(ref ClientCount), x); + }); + _ = hubConnection.StartAsync(); + + while (true) + { + try + { + await hubConnection.SendAsync(nameof(LocalHub.Foo)); + } + catch (Exception e) + { + Console.WriteLine(e); + } + await Task.Delay(1000); + } + } + + private static async Task TriggerIngressReloadAfter(string podName, int millisecondsDelay) + { + await Task.Delay(millisecondsDelay); + + var url = $"http://localhost:5003/notify/ingress-controller/reload?podName={podName}"; + + var httpClient = new HttpClient(); + await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, url) + { + Content = JsonContent.Create(podName) + }); + } + + public class IngressRequest + { + [JsonProperty("podName")] + public string PodName { get; set; } = string.Empty; + } + + private sealed class LocalHub : Hub + { + public Task Foo() + { + var index = Interlocked.Increment(ref ServerCount); + return Clients.Caller.SendAsync("bar", index); + } + + public override Task OnConnectedAsync() + { + return Task.CompletedTask; + } + } + + private sealed class EndlessConnectionHandler : ConnectionHandler where THub : Hub + { + public ServiceProvider ServiceProvider { get; } + + public EndlessConnectionHandler(ILoggerFactory loggerFactory) + { + var collection = new ServiceCollection(); + collection.AddLogging(); + collection.AddSingleton(loggerFactory); + collection.AddSingleton(); + collection.AddSingleton>(); + collection.AddSignalR().AddJsonProtocol(); + + ServiceProvider = collection.BuildServiceProvider(); + } + + public override async Task OnConnectedAsync(ConnectionContext connection) + { + var handler = ServiceProvider.GetRequiredService>(); + await handler.OnConnectedAsync(connection); + } + } + + private sealed class EndlessHubConnectionContext(ConnectionContext connectionContext, + HubConnectionContextOptions contextOptions, + ILoggerFactory loggerFactory) : HubConnectionContext(connectionContext, contextOptions, loggerFactory) + { + } +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/SignalRServiceE2EFacts.cs b/test/Microsoft.Azure.SignalR.E2ETests/SignalR/SignalRServiceE2EFacts.cs deleted file mode 100644 index 4c8ed9235..000000000 --- a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/SignalRServiceE2EFacts.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Testing.xunit; -using Microsoft.Azure.SignalR.Tests.Common; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.Azure.SignalR.Tests -{ - public class SignalRServiceE2EFacts : ServiceE2EFactsBase - { - public static object[][] TestData = TestDataBase.Concat(new object[][] { - new object[] { "TestClientIPEcho", DefaultClientCount, new Func((methodName, clients) => clients.SendAsync(methodName, sendCount : DefaultClientCount, messages : DefaultMessage)) }, - new object[] { "TestClientUser", DefaultClientCount, new Func((methodName, clients) => clients.SendAsync(methodName, sendCount : DefaultClientCount, messages : DefaultMessage)) }, - new object[] { "TestClientQueryString", DefaultClientCount, new Func((methodName, clients) => clients.SendAsync(methodName, sendCount : DefaultClientCount, messages : DefaultMessage)) }, - }).ToArray(); - - public SignalRServiceE2EFacts(ITestOutputHelper output) : base(new TestServerFactory(), new TestClientSetFactory(), output) - { - } - - [ConditionalTheory] - [SkipIfConnectionStringNotPresent] - [MemberData(nameof(TestData))] - public Task RunE2ETests(string methodName, int expectedMessageCount, Func coreTask) - { - return RunE2ETestsBase(methodName, expectedMessageCount, coreTask); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestClientSet.cs b/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestClientSet.cs deleted file mode 100644 index df8259532..000000000 --- a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestClientSet.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Azure.SignalR.Tests.Common; -using Xunit.Abstractions; - -namespace Microsoft.Azure.SignalR.Tests -{ - internal sealed class TestClientSet : ITestClientSet - { - private readonly IList _connections; - - private readonly ITestOutputHelper _output; - - public int Count => _connections?.Count ?? 0; - - public TestClientSet(string serverUrl, int count, ITestOutputHelper output) - { - _output = output; -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(serverUrl); -#else - if (serverUrl == null) - { - throw new ArgumentNullException(nameof(serverUrl)); - } -#endif - - _connections = (from i in Enumerable.Range(0, count) - select new HubConnectionBuilder().WithUrl($"{serverUrl}/{nameof(TestHub)}?user=user_{i}").Build()).ToList(); - - foreach (var conn in _connections) - { - conn.Closed += ex => - { - if (ex != null) - { - _output.WriteLine($"Client connection closed: {ex}"); - } - return Task.CompletedTask; - }; - } - } - - public Task StartAsync() - { - return Task.WhenAll(from conn in _connections select conn.StartAsync()); - } - - public Task StopAsync() - { - return Task.WhenAll(from conn in _connections select conn.StopAsync()); - } - - public void AddListener(string methodName, Action handler) - { - foreach (var conn in _connections) - { - conn.On(methodName, handler); - } - } - - public Task SendAsync(string methodName, int sendCount, params string[] messages) - { - return Task.WhenAll(_connections - .Where((_, i) => sendCount == -1 || i < sendCount) - .Select(conn => conn.SendCoreAsync(methodName, messages))); - } - - public Task SendAsync(string methodName, int[] sendInds, params string[] messages) - { - return Task.WhenAll(_connections - .Where((_, i) => sendInds.Contains(i)) - .Select(conn => conn.SendCoreAsync(methodName, messages))); - } - - public Task ManageGroupAsync(string methodName, IDictionary connectionGroupMap) - { - return Task.WhenAll(from entry in connectionGroupMap - let connInd = entry.Key - let groupName = entry.Value - select _connections[connInd].SendAsync(methodName, groupName)); - } - } -} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestClientSetFactory.cs b/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestClientSetFactory.cs deleted file mode 100644 index b4494e23f..000000000 --- a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestClientSetFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Azure.SignalR.Tests.Common; -using Xunit.Abstractions; - -namespace Microsoft.Azure.SignalR.Tests -{ - internal sealed class TestClientSetFactory : ITestClientSetFactory - { - public ITestClientSet Create(string serverUrl, int count, ITestOutputHelper output) - { - return new TestClientSet(serverUrl, count, output); - } - } -} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestHub.cs b/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestHub.cs deleted file mode 100644 index 9ecfb7fca..000000000 --- a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestHub.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Azure.SignalR.Tests.Common; - -namespace Microsoft.Azure.SignalR.Tests -{ - internal sealed class TestHub : Hub - { - private readonly TestHubConnectionManager _testHubConnectionManager; - - public TestHub(TestHubConnectionManager testHubConnectionManager) - { - _testHubConnectionManager = testHubConnectionManager; - } - - public override Task OnConnectedAsync() - { - _testHubConnectionManager.AddClient(Context.ConnectionId); - _testHubConnectionManager.AddUser(Context.UserIdentifier); - return Task.CompletedTask; - } - - public override Task OnDisconnectedAsync(Exception exception) - { - _testHubConnectionManager.RemoveClient(Context.ConnectionId); - _testHubConnectionManager.RemoveUser(Context.UserIdentifier); - return Task.CompletedTask; - } - - // Verify whether 'get client IP' is working or not - public void TestClientIPEcho(string message) - { - if (!string.IsNullOrEmpty(Context.GetHttpContext().Connection.RemoteIpAddress?.ToString())) - { - Clients.Caller.SendAsync(nameof(TestClientIPEcho), message); - } - } - - public void TestClientUser(string message) - { - if (Context.User != null) - { - Clients.Caller.SendAsync(nameof(TestClientUser), message); - } - } - - public void TestClientQueryString(string message) - { - if (Context.GetHttpContext().Request.QueryString.Value != null) - { - Clients.Caller.SendAsync(nameof(TestClientQueryString), message); - } - } - - public void Echo(string message) - { - Clients.Caller.SendAsync(nameof(Echo), message); - } - - public void SendToClient(string message) - { - var ind = StaticRandom.Next(0, _testHubConnectionManager.ClientCount); - Clients.Client(_testHubConnectionManager.Clients[ind]).SendAsync(nameof(SendToClient), message); - } - - public void SendToUser(string message) - { - var ind = StaticRandom.Next(0, _testHubConnectionManager.UserCount); - Clients.User(_testHubConnectionManager.Users[ind]).SendAsync(nameof(SendToUser), message); - } - - public void Broadcast(string message) - { - Clients.All.SendAsync(nameof(Broadcast), message); - } - - public Task JoinGroup(string groupName) - { - return Groups.AddToGroupAsync(Context.ConnectionId, groupName); - } - - public Task LeaveGroup(string groupName) - { - return Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); - } - - public void SendToGroup(string groupName, string message) - { - Clients.Group(groupName).SendAsync(nameof(SendToGroup), message); - } - } -} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestServer.cs b/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestServer.cs deleted file mode 100644 index 79247d696..000000000 --- a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestServer.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Azure.SignalR.Tests.Common; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Xunit.Abstractions; - -namespace Microsoft.Azure.SignalR.Tests -{ - internal sealed class TestServer : TestServerBase - { - private IWebHost _host; - - private IServiceConnectionManager _scm; - - public override TestHubConnectionManager HubConnectionManager { get; } - - public TestServer(ITestOutputHelper output) : base(output) - { - HubConnectionManager = new TestHubConnectionManager(); - } - - public override async Task StopAsync() - { - await _host.StopAsync(); - await _scm.StopAsync(); - } - - protected override Task StartCoreAsync(string serverUrl, ITestOutputHelper output, Dictionary configuration) - { - _host = new WebHostBuilder() - .ConfigureServices(services => - { - services.AddSingleton(HubConnectionManager); - }) - .ConfigureLogging(logging => logging.AddXunit(output)) - .ConfigureAppConfiguration(builder => builder.AddInMemoryCollection(configuration)) - .UseStartup() - .UseUrls(serverUrl) - .UseKestrel() - .Build(); - - _scm = _host.Services.GetRequiredService>(); - - return _host.StartAsync(); - } - } -} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestServerFactory.cs b/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestServerFactory.cs deleted file mode 100644 index 0ac3954bc..000000000 --- a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestServerFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Azure.SignalR.Tests.Common; -using Xunit.Abstractions; - -namespace Microsoft.Azure.SignalR.Tests -{ - internal sealed class TestServerFactory : ITestServerFactory - { - public ITestServer Create(ITestOutputHelper output) - { - return new TestServer(output); - } - } -} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestStartup.cs b/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestStartup.cs deleted file mode 100644 index bbaf5c755..000000000 --- a/test/Microsoft.Azure.SignalR.E2ETests/SignalR/TestStartup.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Security.Claims; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Azure.SignalR.Tests.Common; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Azure.SignalR.Tests -{ - internal sealed class TestStartup : IStartup - { - public const string ApplicationName = "AppName"; - - private readonly IConfiguration _configuration; - - public TestStartup(IConfiguration configuration) - { - _configuration = configuration; - } - - public void Configure(IApplicationBuilder app) - { - app.UseRouting(); - app.UseEndpoints(configure => configure.MapHub($"/{nameof(TestHub)}")); - app.UseMvc(); - } - - public IServiceProvider ConfigureServices(IServiceCollection services) - { - var applicationName = _configuration[ApplicationName]; - - services.AddMvc(option => option.EnableEndpointRouting = false); - services - .AddSignalR(options => - { - options.EnableDetailedErrors = true; - }) - .AddAzureSignalR(o => - { - o.ConnectionString = TestConfiguration.Instance.ConnectionString; - o.ClaimsProvider = context => new[] { new Claim(ClaimTypes.NameIdentifier, context.Request.Query["user"]) }; - o.ApplicationName = applicationName; - }); - - return services.BuildServiceProvider(); - } - } -} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/TestHub.cs b/test/Microsoft.Azure.SignalR.E2ETests/TestHub.cs new file mode 100644 index 000000000..a14f40bea --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/TestHub.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.SignalR; + +namespace Microsoft.Azure.SignalR.E2ETests; + +internal sealed class TestHub : Hub +{ + public void Echo(string message) + { + Clients.Caller.SendAsync(nameof(Echo), message); + } +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/TestServer.cs b/test/Microsoft.Azure.SignalR.E2ETests/TestServer.cs new file mode 100644 index 000000000..f91d6cb74 --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/TestServer.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Azure.SignalR.E2ETests.Common; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +using Xunit.Abstractions; + +namespace Microsoft.Azure.SignalR.E2ETests; + +#nullable enable + +internal sealed class TestServer(ITestOutputHelper output) : ITestServer, IDisposable +{ + private const int MaxRetry = 10; + + private static int CurrentPortOffset = 11000; + + private readonly ITestOutputHelper _output = output; + + private IWebHost? _host; + + public bool EnableStatefulReconnect { get; init; } + + public string? ConnectionString { get; init; } + + public async Task StartAsync(Dictionary? configuration = null) + { + for (var retry = 0; retry < MaxRetry; retry++) + { + try + { + var serverUrl = GetUrl(); + await StartCoreAsync(serverUrl, _output, configuration); + _output.WriteLine($"Server started: {serverUrl}"); + return serverUrl; + } + catch (IOException ex) + { + if (ex.Message.Contains("address already in use") || + ex.Message.Contains("Failed to bind to address")) + { + _output.WriteLine($"Retry: {retry + 1} times. Warning: {ex.Message}"); + } + else + { + throw; + } + } + } + + throw new IOException($"Fail to start server for {MaxRetry} times. Ports are already in used"); + } + + public void Dispose() => _host?.Dispose(); + + public async Task StopAsync() + { + if (_host != null) + { + await _host.StopAsync(); + } + } + + private static int GetPortOffset() => Interlocked.Increment(ref CurrentPortOffset); + + private static string GetUrl() + { + return $"http://localhost:{GetPortOffset()}"; + } + + private Task StartCoreAsync(string serverUrl, ITestOutputHelper output, Dictionary? configuration) + { + configuration ??= []; + if (ConnectionString != null) + { + configuration[TestStartup.ConnectionString] = ConnectionString; + } + _host = new WebHostBuilder() + .ConfigureLogging(logging => logging.AddXunit(output)) + .ConfigureAppConfiguration(builder => builder.AddInMemoryCollection(configuration)) + .UseStartup() + .UseUrls(serverUrl) + .UseKestrel() + .Build(); + return _host.StartAsync(); + } +} diff --git a/test/Microsoft.Azure.SignalR.E2ETests/TestStartup.cs b/test/Microsoft.Azure.SignalR.E2ETests/TestStartup.cs new file mode 100644 index 000000000..08185c6ee --- /dev/null +++ b/test/Microsoft.Azure.SignalR.E2ETests/TestStartup.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Security.Claims; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Azure.SignalR.E2ETests.Common; +using Microsoft.Azure.SignalR.Tests.Common; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Azure.SignalR.E2ETests; + +internal sealed class TestStartup(IConfiguration configuration) : IStartup +{ + public const string ApplicationName = "AppName"; + + public const string ConnectionString = "ConnectionString"; + + private readonly IConfiguration _configuration = configuration; + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(configure => configure.MapHub($"/{nameof(TestHub)}")); + app.UseMvc(); + } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + var applicationName = _configuration[ApplicationName]; + var connectionString = _configuration[ConnectionString] ?? TestConfiguration.Instance.ConnectionString; + + services.AddMvc(option => option.EnableEndpointRouting = false); + services + .AddSignalR(options => options.EnableDetailedErrors = true) + .AddAzureSignalR(o => + { + o.ConnectionString = connectionString; + o.ClaimsProvider = context => [new Claim(ClaimTypes.NameIdentifier, context.Request.Query["user"].ToString())]; + o.ApplicationName = applicationName; + // o.GracefulShutdown = new GracefulShutdownOptions() + // { + // Mode = GracefulShutdownMode.MigrateClients, + // }; + }); + + services.AddSingleton(); + return services.BuildServiceProvider(); + } +} diff --git a/test/Microsoft.Azure.SignalR.IntegrationTests/Infrastructure/MockServiceConnectionFactory.cs b/test/Microsoft.Azure.SignalR.IntegrationTests/Infrastructure/MockServiceConnectionFactory.cs index ed185c73f..a5f70e399 100644 --- a/test/Microsoft.Azure.SignalR.IntegrationTests/Infrastructure/MockServiceConnectionFactory.cs +++ b/test/Microsoft.Azure.SignalR.IntegrationTests/Infrastructure/MockServiceConnectionFactory.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.AspNetCore.Connections; @@ -9,35 +9,27 @@ namespace Microsoft.Azure.SignalR.IntegrationTests.Infrastructure; -internal class MockServiceConnectionFactory : ServiceConnectionFactory +internal class MockServiceConnectionFactory(IMockService mockService, + IServiceProtocol serviceProtocol, + IClientConnectionManager clientConnectionManager, + IConnectionFactory connectionFactory, + ILoggerFactory loggerFactory, + ConnectionDelegate connectionDelegate, + IClientConnectionFactory clientConnectionFactory, + IClientInvocationManager clientInvocationManager, + IServerNameProvider nameProvider, + IHubProtocolResolver hubProtocolResolver) : ServiceConnectionFactory( + serviceProtocol, + clientConnectionManager, + connectionFactory, + loggerFactory, + connectionDelegate, + clientConnectionFactory, + nameProvider, + null, + clientInvocationManager, + hubProtocolResolver) { - private IMockService _mockService; - public MockServiceConnectionFactory( - IMockService mockService, - IServiceProtocol serviceProtocol, - IClientConnectionManager clientConnectionManager, - IConnectionFactory connectionFactory, - ILoggerFactory loggerFactory, - ConnectionDelegate connectionDelegate, - IClientConnectionFactory clientConnectionFactory, - IClientInvocationManager clientInvocationManager, - IServerNameProvider nameProvider, - IHubProtocolResolver hubProtocolResolver) - : base( - serviceProtocol, - clientConnectionManager, - connectionFactory, - loggerFactory, - connectionDelegate, - clientConnectionFactory, - nameProvider, - null, - clientInvocationManager, - hubProtocolResolver, - null) - { - _mockService = mockService; - } public override IServiceConnection Create(HubServiceEndpoint endpoint, IServiceMessageHandler serviceMessageHandler, AckHandler ackHandler, ServiceConnectionType type) { diff --git a/test/Microsoft.Azure.SignalR.IntegrationTests/Infrastructure/MockServiceHubDispatcher.cs b/test/Microsoft.Azure.SignalR.IntegrationTests/Infrastructure/MockServiceHubDispatcher.cs index 20f2126d7..51b0cbc9e 100644 --- a/test/Microsoft.Azure.SignalR.IntegrationTests/Infrastructure/MockServiceHubDispatcher.cs +++ b/test/Microsoft.Azure.SignalR.IntegrationTests/Infrastructure/MockServiceHubDispatcher.cs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; + using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.SignalR; using Microsoft.Azure.SignalR.IntegrationTests.MockService; @@ -10,75 +11,62 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Microsoft.Azure.SignalR.IntegrationTests.Infrastructure +namespace Microsoft.Azure.SignalR.IntegrationTests.Infrastructure; + +internal class MockServiceHubDispatcher( + IMockService mockService, + IServiceProtocol serviceProtocol, + IHubContext context, + IServiceConnectionManager serviceConnectionManager, + IClientConnectionManager clientConnectionManager, + IClientInvocationManager clientInvocationManager, + IServiceEndpointManager serviceEndpointManager, + IOptions options, + ILoggerFactory loggerFactory, + IEndpointRouter router, + IServerNameProvider nameProvider, + ServerLifetimeManager serverLifetimeManager, + IClientConnectionFactory clientConnectionFactory, + IConnectionFactory connectionFactory, + IServiceProvider serviceProvider, + IHubProtocolResolver hubProtocolResolver) : ServiceHubDispatcher( + serviceProtocol, + context, + serviceConnectionManager, + clientConnectionManager, + serviceEndpointManager, + options, + loggerFactory, + router, + nameProvider, + serverLifetimeManager, + clientConnectionFactory, + clientInvocationManager, + null, + hubProtocolResolver, + connectionFactory, + serviceProvider, + null) where THub : Hub { - internal class MockServiceHubDispatcher : ServiceHubDispatcher - where THub : Hub - { - private ILoggerFactory _loggerFactory; - private IClientConnectionManager _clientConnectionManager; - private IServiceProtocol _serviceProtocol; - private IClientConnectionFactory _clientConnectionFactory; - private readonly IServiceProvider _serviceProvider; - private IClientInvocationManager _clientInvocationManager; - private IHubProtocolResolver _hubProtocolResolver; + private readonly IServiceProvider _serviceProvider = serviceProvider; + + private ILoggerFactory _loggerFactory = loggerFactory; - public MockServiceHubDispatcher( - IMockService mockService, - IServiceProtocol serviceProtocol, - IHubContext context, - IServiceConnectionManager serviceConnectionManager, - IClientConnectionManager clientConnectionManager, - IClientInvocationManager clientInvocationManager, - IServiceEndpointManager serviceEndpointManager, - IOptions options, - ILoggerFactory loggerFactory, - IEndpointRouter router, - IServerNameProvider nameProvider, - ServerLifetimeManager serverLifetimeManager, - IClientConnectionFactory clientConnectionFactory, - IConnectionFactory connectionFactory, - IServiceProvider serviceProvider, - IHubProtocolResolver hubProtocolResolver) : base( - serviceProtocol, - context, - serviceConnectionManager, - clientConnectionManager, - serviceEndpointManager, - options, - loggerFactory, - router, - nameProvider, - serverLifetimeManager, - clientConnectionFactory, - clientInvocationManager, - null, - hubProtocolResolver, - connectionFactory, - serviceProvider, - null) - { - MockService = mockService; + private IClientConnectionManager _clientConnectionManager = clientConnectionManager; - // just store copies of these locally to keep the base class' accessor modifiers intact - _loggerFactory = loggerFactory; - _clientConnectionManager = clientConnectionManager; - _serviceProtocol = serviceProtocol; - _clientConnectionFactory = clientConnectionFactory; - _serviceProvider = serviceProvider; - _clientInvocationManager = clientInvocationManager; - _hubProtocolResolver = hubProtocolResolver; - } + private IServiceProtocol _serviceProtocol = serviceProtocol; - internal override ServiceConnectionFactory GetServiceConnectionFactory(ConnectionDelegate connectionDelegate) - { - return ActivatorUtilities.CreateInstance(_serviceProvider, connectionDelegate); - } + private IClientConnectionFactory _clientConnectionFactory = clientConnectionFactory; - // this is the gateway for the tests to control the mock service side - public IMockService MockService { - get; - private set; - } + private IClientInvocationManager _clientInvocationManager = clientInvocationManager; + + private IHubProtocolResolver _hubProtocolResolver = hubProtocolResolver; + + // this is the gateway for the tests to control the mock service side + public IMockService MockService { get; private set; } = mockService; + + internal override ServiceConnectionFactory GetServiceConnectionFactory(ConnectionDelegate connectionDelegate) + { + return ActivatorUtilities.CreateInstance(_serviceProvider, connectionDelegate); } } diff --git a/test/Microsoft.Azure.SignalR.Tests.Common/SkipIfConnectionStringNotPresentAttribute.cs b/test/Microsoft.Azure.SignalR.Tests.Common/SkipIfConnectionStringNotPresentAttribute.cs index 323efc7a0..ad3a0cf22 100644 --- a/test/Microsoft.Azure.SignalR.Tests.Common/SkipIfConnectionStringNotPresentAttribute.cs +++ b/test/Microsoft.Azure.SignalR.Tests.Common/SkipIfConnectionStringNotPresentAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; + using Microsoft.AspNetCore.Testing.xunit; namespace Microsoft.Azure.SignalR.Tests.Common diff --git a/test/Microsoft.Azure.SignalR.Tests.Common/TestConfiguration.cs b/test/Microsoft.Azure.SignalR.Tests.Common/TestConfiguration.cs index 6d94d08b6..0cd4316d1 100644 --- a/test/Microsoft.Azure.SignalR.Tests.Common/TestConfiguration.cs +++ b/test/Microsoft.Azure.SignalR.Tests.Common/TestConfiguration.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.IO; + using Microsoft.Extensions.Configuration; namespace Microsoft.Azure.SignalR.Tests.Common diff --git a/test/Microsoft.Azure.SignalR.Tests/ServiceConnectionTests.cs b/test/Microsoft.Azure.SignalR.Tests/ServiceConnectionTests.cs index 65b04aa4b..7736f06b3 100644 --- a/test/Microsoft.Azure.SignalR.Tests/ServiceConnectionTests.cs +++ b/test/Microsoft.Azure.SignalR.Tests/ServiceConnectionTests.cs @@ -26,7 +26,9 @@ using SignalRProtocol = Microsoft.AspNetCore.SignalR.Protocol; namespace Microsoft.Azure.SignalR.Tests; + #nullable disable + public class ServiceConnectionTests : VerifiableLoggedTest { public ServiceConnectionTests(ITestOutputHelper output) : base(output) diff --git a/test/appsettings.Test.json b/test/appsettings.Test.json index c9408e0b8..129efeb35 100644 --- a/test/appsettings.Test.json +++ b/test/appsettings.Test.json @@ -1,8 +1,13 @@ -{ +{ "ReadMe": "Recommend external contributors fill in `ConnectionString`, change service mode to `Classic` and then run E2E test before create a pull request.", "Azure": { "SignalR": { - "ConnectionString": "" + "ConnectionString": "Endpoint=http://localhost:8080;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH;Version=1.0;" } - } + }, + "Logging": { + "LogLevel": { + "Microsoft.Azure.SignalR": "Information" + } + }, }