From 5a36ceb67be98e63ec1411bc5ff7855ecfb613e2 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Thu, 5 Sep 2019 17:01:43 +0800 Subject: [PATCH 1/3] Wanl/fix attribute connection string setting bug (#72) * update * Add e2e test for attri para * add ut for signalr connection info attribute * update * update mock test * update * ServiceManagerStore todo: 1. update hostjobtest 2. test function.cs * update tests * fix test * modify interface * rename: StaticServiceManagerStore -> StaticServiceHubContextStore * update test * update sample * remove setter --- samples/simple-chat/content/index.html | 2 +- .../csharp/FunctionApp/Functions.cs | 28 +- .../Bindings/SignalRAsyncCollector.cs | 20 +- .../Bindings/SignalRCollectorBuilder.cs | 4 +- .../Client/AzureSignalRClient.cs | 57 ++-- .../Client/IAzureSignalRSender.cs | 16 +- .../Config/IServiceHubContextStore.cs | 19 +- .../Config/IServiceManagerStore.cs | 12 + .../Config/ServiceHubContextStore.cs | 11 +- .../Config/ServiceManagerStore.cs | 58 +++++ .../Config/SignalRConfigProvider.cs | 52 ++-- .../Config/StaticServiceHubContextStore.cs | 21 +- src/SignalRServiceExtension/Constants.cs | 11 + src/SignalRServiceExtension/ErrorMessages.cs | 11 + .../SignalRAttribute.cs | 2 +- .../SignalRConnectionInfoAttribute.cs | 2 +- .../AzureSignalRClientTests.cs | 12 +- .../JobhostEndToEnd.cs | 243 ++++++++++++++++++ .../SignalRAsyncCollectorTests.cs | 72 +++--- .../Utils/FakeTypeLocator.cs | 24 ++ .../Utils/Loggings/XunitLogger.cs | 41 +++ .../Utils/Loggings/XunitLoggerProvider.cs | 24 ++ .../Utils/TestExtensionConfig.cs | 31 +++ .../Utils/TestHelpers.cs | 64 +++-- 24 files changed, 670 insertions(+), 167 deletions(-) create mode 100644 src/SignalRServiceExtension/Config/IServiceManagerStore.cs create mode 100644 src/SignalRServiceExtension/Config/ServiceManagerStore.cs create mode 100644 src/SignalRServiceExtension/Constants.cs create mode 100644 src/SignalRServiceExtension/ErrorMessages.cs create mode 100644 test/SignalRServiceExtension.Tests/JobhostEndToEnd.cs create mode 100644 test/SignalRServiceExtension.Tests/Utils/FakeTypeLocator.cs create mode 100644 test/SignalRServiceExtension.Tests/Utils/Loggings/XunitLogger.cs create mode 100644 test/SignalRServiceExtension.Tests/Utils/Loggings/XunitLoggerProvider.cs create mode 100644 test/SignalRServiceExtension.Tests/Utils/TestExtensionConfig.cs diff --git a/samples/simple-chat/content/index.html b/samples/simple-chat/content/index.html index 65937140..a4b987c6 100644 --- a/samples/simple-chat/content/index.html +++ b/samples/simple-chat/content/index.html @@ -173,7 +173,7 @@

Serverless chat

return config; } function getConnectionInfo() { - return axios.post(`${apiBaseUrl}/api/negotiate`, null, getAxiosConfig()) + return axios.post(`${apiBaseUrl}/api/negotiate?userid=${data.username}&hubname=simplechat`, null, getAxiosConfig()) .then(resp => resp.data); } function sendMessage(sender, recipient, groupname, messageText) { diff --git a/samples/simple-chat/csharp/FunctionApp/Functions.cs b/samples/simple-chat/csharp/FunctionApp/Functions.cs index 1876a9db..5adf6c20 100644 --- a/samples/simple-chat/csharp/FunctionApp/Functions.cs +++ b/samples/simple-chat/csharp/FunctionApp/Functions.cs @@ -2,12 +2,15 @@ // 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.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; using Microsoft.Azure.EventGrid.Models; +using Microsoft.Azure.SignalR.Common; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.EventGrid; using Microsoft.Azure.WebJobs.Extensions.Http; @@ -27,13 +30,32 @@ public static SignalRConnectionInfo GetSignalRInfo( return connectionInfo; } + //// Each function must have a unique name, you can uncomment this one and comment the above GetSignalRInfo() function to have a try. + //// This "negotiate" function shows how to utilize ServiceManager to generate access token and client url to Azure SignalR service. + //[FunctionName("negotiate")] + //public static SignalRConnectionInfo GetSignalRInfo( + // [HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req) + //{ + // var userId = req.Query["userid"]; + // var hubName = req.Query["hubname"]; + // var connectionInfo = new SignalRConnectionInfo(); + // var serviceManager = StaticServiceHubContextStore.Get().ServiceManager; + // connectionInfo.AccessToken = serviceManager + // .GenerateClientAccessToken( + // hubName, + // userId, + // new List { new Claim("claimType", "claimValue") }); + // connectionInfo.Url = serviceManager.GetClientEndpoint(hubName); + // return connectionInfo; + //} + [FunctionName("broadcast")] public static async Task Broadcast( [HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequest req, [SignalR(HubName = "simplechat")]IAsyncCollector signalRMessages) { var message = new JsonSerializer().Deserialize(new JsonTextReader(new StreamReader(req.Body))); - var serviceHubContext = await StaticServiceHubContextStore.GetOrAddAsync("simplechat"); + var serviceHubContext = await StaticServiceHubContextStore.Get().GetAsync("simplechat"); await serviceHubContext.Clients.All.SendAsync("newMessage", message); } @@ -111,7 +133,7 @@ private static string GetBase64DecodedString(string source) return source; } - return Encoding.UTF8.GetString(Convert.FromBase64String(source)); + return Encoding.UTF8.GetString(Convert.FromBase64String(source)); } @@ -123,7 +145,7 @@ public static Task EventGridTest([EventGridTrigger]EventGridEvent eventGridEvent { if (eventGridEvent.EventType == "Microsoft.SignalRService.ClientConnectionConnected") { - var message = ((JObject) eventGridEvent.Data).ToObject(); + var message = ((JObject)eventGridEvent.Data).ToObject(); return signalRMessages.AddAsync( new SignalRMessage diff --git a/src/SignalRServiceExtension/Bindings/SignalRAsyncCollector.cs b/src/SignalRServiceExtension/Bindings/SignalRAsyncCollector.cs index db47d259..92f3183b 100644 --- a/src/SignalRServiceExtension/Bindings/SignalRAsyncCollector.cs +++ b/src/SignalRServiceExtension/Bindings/SignalRAsyncCollector.cs @@ -10,13 +10,11 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService public class SignalRAsyncCollector : IAsyncCollector { private readonly IAzureSignalRSender client; - private readonly string hubName; private readonly SignalROutputConverter converter; - internal SignalRAsyncCollector(IAzureSignalRSender client, string hubName) + internal SignalRAsyncCollector(IAzureSignalRSender client) { this.client = client; - this.hubName = hubName; converter = new SignalROutputConverter(); } @@ -40,19 +38,19 @@ internal SignalRAsyncCollector(IAzureSignalRSender client, string hubName) if (!string.IsNullOrEmpty(message.ConnectionId)) { - await client.SendToConnection(hubName, message.ConnectionId, data).ConfigureAwait(false); + await client.SendToConnection(message.ConnectionId, data).ConfigureAwait(false); } else if (!string.IsNullOrEmpty(message.UserId)) { - await client.SendToUser(hubName, message.UserId, data).ConfigureAwait(false); + await client.SendToUser(message.UserId, data).ConfigureAwait(false); } else if (!string.IsNullOrEmpty(message.GroupName)) { - await client.SendToGroup(hubName, message.GroupName, data).ConfigureAwait(false); + await client.SendToGroup(message.GroupName, data).ConfigureAwait(false); } else { - await client.SendToAll(hubName, data).ConfigureAwait(false); + await client.SendToAll(data).ConfigureAwait(false); } } else if (convertItem.GetType() == typeof(SignalRGroupAction)) @@ -63,22 +61,22 @@ internal SignalRAsyncCollector(IAzureSignalRSender client, string hubName) { if (groupAction.Action == GroupAction.Add) { - await client.AddConnectionToGroup(hubName, groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false); + await client.AddConnectionToGroup(groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false); } else { - await client.RemoveConnectionFromGroup(hubName, groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false); + await client.RemoveConnectionFromGroup(groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false); } } else if (!string.IsNullOrEmpty(groupAction.UserId)) { if (groupAction.Action == GroupAction.Add) { - await client.AddUserToGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); + await client.AddUserToGroup(groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); } else { - await client.RemoveUserFromGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); + await client.RemoveUserFromGroup(groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); } } else diff --git a/src/SignalRServiceExtension/Bindings/SignalRCollectorBuilder.cs b/src/SignalRServiceExtension/Bindings/SignalRCollectorBuilder.cs index 658b1a8c..493132ec 100644 --- a/src/SignalRServiceExtension/Bindings/SignalRCollectorBuilder.cs +++ b/src/SignalRServiceExtension/Bindings/SignalRCollectorBuilder.cs @@ -14,8 +14,8 @@ public SignalRCollectorBuilder(SignalRConfigProvider configProvider) public IAsyncCollector Convert(SignalRAttribute attribute) { - var client = configProvider.GetClient(attribute); - return new SignalRAsyncCollector(client, attribute.HubName); + var client = configProvider.GetAzureSignalRClient(attribute.ConnectionStringSetting, attribute.HubName); + return new SignalRAsyncCollector(client); } } } diff --git a/src/SignalRServiceExtension/Client/AzureSignalRClient.cs b/src/SignalRServiceExtension/Client/AzureSignalRClient.cs index c7bd6bf3..ec062216 100644 --- a/src/SignalRServiceExtension/Client/AzureSignalRClient.cs +++ b/src/SignalRServiceExtension/Client/AzureSignalRClient.cs @@ -5,17 +5,16 @@ using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; using System.Security.Claims; -using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Azure.SignalR.Management; -using Microsoft.IdentityModel.Tokens; namespace Microsoft.Azure.WebJobs.Extensions.SignalRService { + /// + /// AzureSignalRClient used for negotiation, publishing messages and managing group membership. + /// It will be created for each function request. + /// internal class AzureSignalRClient : IAzureSignalRSender { public const string AzureSignalRUserPrefix = "asrs.u."; @@ -26,16 +25,18 @@ internal class AzureSignalRClient : IAzureSignalRSender "iat", // Issued At claim. Added by default. It is not validated by service. "nbf" // Not Before claim. Added by default. It is not validated by service. }; - private readonly IServiceHubContextStore serviceHubContextStore; - private readonly IServiceManager serviceManager; + private readonly IServiceManagerStore serviceManagerStore; + private readonly string hubName; + private readonly string connectionString; - internal AzureSignalRClient(IServiceHubContextStore serviceHubContextStore, IServiceManager serviceManager) + internal AzureSignalRClient(IServiceManagerStore serviceManagerStore, string connectionString, string hubName) { - this.serviceHubContextStore = serviceHubContextStore; - this.serviceManager = serviceManager; + this.serviceManagerStore = serviceManagerStore; + this.hubName = hubName; + this.connectionString = connectionString; } - public SignalRConnectionInfo GetClientConnectionInfo(string hubName, string userId, string idToken, string[] claimTypeList) + public SignalRConnectionInfo GetClientConnectionInfo(string userId, string idToken, string[] claimTypeList) { IEnumerable customerClaims = null; if (idToken != null && claimTypeList != null && claimTypeList.Length > 0) @@ -46,6 +47,8 @@ where claimTypeList.Contains(claim.Type) select claim; } + var serviceManager = serviceManagerStore.GetOrAddByConnectionString(connectionString).ServiceManager; + return new SignalRConnectionInfo { Url = serviceManager.GetClientEndpoint(hubName), @@ -54,39 +57,39 @@ where claimTypeList.Contains(claim.Type) }; } - public async Task SendToAll(string hubName, SignalRData data) + public async Task SendToAll(SignalRData data) { - var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName); + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); await serviceHubContext.Clients.All.SendCoreAsync(data.Target, data.Arguments); } - public async Task SendToConnection(string hubName, string connectionId, SignalRData data) + public async Task SendToConnection(string connectionId, SignalRData data) { - var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName); + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); await serviceHubContext.Clients.Client(connectionId).SendCoreAsync(data.Target, data.Arguments); } - public async Task SendToUser(string hubName, string userId, SignalRData data) + public async Task SendToUser(string userId, SignalRData data) { if (string.IsNullOrEmpty(userId)) { throw new ArgumentException($"{nameof(userId)} cannot be null or empty"); } - var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName); + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); await serviceHubContext.Clients.User(userId).SendCoreAsync(data.Target, data.Arguments); } - public async Task SendToGroup(string hubName, string groupName, SignalRData data) + public async Task SendToGroup(string groupName, SignalRData data) { if (string.IsNullOrEmpty(groupName)) { throw new ArgumentException($"{nameof(groupName)} cannot be null or empty"); } - var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName); + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); await serviceHubContext.Clients.Group(groupName).SendCoreAsync(data.Target, data.Arguments); } - public async Task AddUserToGroup(string hubName, string userId, string groupName) + public async Task AddUserToGroup(string userId, string groupName) { if (string.IsNullOrEmpty(userId)) { @@ -96,11 +99,11 @@ public async Task AddUserToGroup(string hubName, string userId, string groupName { throw new ArgumentException($"{nameof(groupName)} cannot be null or empty"); } - var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName); + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); await serviceHubContext.UserGroups.AddToGroupAsync(userId, groupName); } - public async Task RemoveUserFromGroup(string hubName, string userId, string groupName) + public async Task RemoveUserFromGroup(string userId, string groupName) { if (string.IsNullOrEmpty(userId)) { @@ -110,11 +113,11 @@ public async Task RemoveUserFromGroup(string hubName, string userId, string grou { throw new ArgumentException($"{nameof(groupName)} cannot be null or empty"); } - var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName); + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); await serviceHubContext.UserGroups.RemoveFromGroupAsync(userId, groupName); } - public async Task AddConnectionToGroup(string hubName, string connectionId, string groupName) + public async Task AddConnectionToGroup(string connectionId, string groupName) { if (string.IsNullOrEmpty(connectionId)) { @@ -124,11 +127,11 @@ public async Task AddConnectionToGroup(string hubName, string connectionId, stri { throw new ArgumentException($"{nameof(groupName)} cannot be null or empty"); } - var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName); + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); await serviceHubContext.Groups.AddToGroupAsync(connectionId, groupName); } - public async Task RemoveConnectionFromGroup(string hubName, string connectionId, string groupName) + public async Task RemoveConnectionFromGroup(string connectionId, string groupName) { if (string.IsNullOrEmpty(connectionId)) { @@ -138,7 +141,7 @@ public async Task RemoveConnectionFromGroup(string hubName, string connectionId, { throw new ArgumentException($"{nameof(groupName)} cannot be null or empty"); } - var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName); + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); await serviceHubContext.Groups.RemoveFromGroupAsync(connectionId, groupName); } diff --git a/src/SignalRServiceExtension/Client/IAzureSignalRSender.cs b/src/SignalRServiceExtension/Client/IAzureSignalRSender.cs index ee21d9fb..47534104 100644 --- a/src/SignalRServiceExtension/Client/IAzureSignalRSender.cs +++ b/src/SignalRServiceExtension/Client/IAzureSignalRSender.cs @@ -8,13 +8,13 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService { internal interface IAzureSignalRSender { - Task SendToAll(string hubName, SignalRData data); - Task SendToConnection(string hubName, string connectionId, SignalRData data); - Task SendToUser(string hubName, string userId, SignalRData data); - Task SendToGroup(string hubName, string group, SignalRData data); - Task AddUserToGroup(string hubName, string userId, string groupName); - Task RemoveUserFromGroup(string hubName, string userId, string groupName); - Task AddConnectionToGroup(string hubName, string connectionId, string groupName); - Task RemoveConnectionFromGroup(string hubName, string connectionId, string groupName); + Task SendToAll(SignalRData data); + Task SendToConnection(string connectionId, SignalRData data); + Task SendToUser(string userId, SignalRData data); + Task SendToGroup(string group, SignalRData data); + Task AddUserToGroup(string userId, string groupName); + Task RemoveUserFromGroup(string userId, string groupName); + Task AddConnectionToGroup(string connectionId, string groupName); + Task RemoveConnectionFromGroup(string connectionId, string groupName); } } \ No newline at end of file diff --git a/src/SignalRServiceExtension/Config/IServiceHubContextStore.cs b/src/SignalRServiceExtension/Config/IServiceHubContextStore.cs index 4188d504..7c181339 100644 --- a/src/SignalRServiceExtension/Config/IServiceHubContextStore.cs +++ b/src/SignalRServiceExtension/Config/IServiceHubContextStore.cs @@ -6,8 +6,23 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService { - internal interface IServiceHubContextStore + /// + /// stores for each hub name. + /// + public interface IServiceHubContextStore { - ValueTask GetOrAddAsync(string hubName); + /// + /// Gets . + /// If the for a specific hub name exists, returns the , + /// otherwise creates one and then returns it. + /// + /// is the hub name of the . + /// The returned value is an instance of . + ValueTask GetAsync(string hubName); + + /// + /// The is used to create . + /// + IServiceManager ServiceManager { get; } } } diff --git a/src/SignalRServiceExtension/Config/IServiceManagerStore.cs b/src/SignalRServiceExtension/Config/IServiceManagerStore.cs new file mode 100644 index 00000000..8faf390e --- /dev/null +++ b/src/SignalRServiceExtension/Config/IServiceManagerStore.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService +{ + internal interface IServiceManagerStore + { + IServiceHubContextStore GetOrAddByConfigurationKey(string configurationKey); + + IServiceHubContextStore GetOrAddByConnectionString(string connectionString); + } +} diff --git a/src/SignalRServiceExtension/Config/ServiceHubContextStore.cs b/src/SignalRServiceExtension/Config/ServiceHubContextStore.cs index 5563f227..b737c875 100644 --- a/src/SignalRServiceExtension/Config/ServiceHubContextStore.cs +++ b/src/SignalRServiceExtension/Config/ServiceHubContextStore.cs @@ -12,18 +12,21 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService internal class ServiceHubContextStore : IServiceHubContextStore { private readonly ConcurrentDictionary> lazy, IServiceHubContext value)> store = new ConcurrentDictionary>, IServiceHubContext value)>(); - private readonly IServiceManager serviceManager; private readonly ILoggerFactory loggerFactory; + public IServiceManager ServiceManager { get; set; } + public ServiceHubContextStore(IServiceManager serviceManager, ILoggerFactory loggerFactory) { - this.serviceManager = serviceManager; + ServiceManager = serviceManager; this.loggerFactory = loggerFactory; } - public ValueTask GetOrAddAsync(string hubName) + public ValueTask GetAsync(string hubName) { - var pair = store.GetOrAdd(hubName, (new Lazy>(() => serviceManager.CreateHubContextAsync(hubName, loggerFactory)), default)); + var pair = store.GetOrAdd(hubName, + (new Lazy>( + () => ServiceManager.CreateHubContextAsync(hubName, loggerFactory)), default)); return GetAsyncCore(hubName, pair); } diff --git a/src/SignalRServiceExtension/Config/ServiceManagerStore.cs b/src/SignalRServiceExtension/Config/ServiceManagerStore.cs new file mode 100644 index 00000000..3762b699 --- /dev/null +++ b/src/SignalRServiceExtension/Config/ServiceManagerStore.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 System.Collections.Concurrent; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService +{ + internal class ServiceManagerStore : IServiceManagerStore + { + private readonly ILoggerFactory loggerFactory; + private readonly ServiceTransportType transportType; + private readonly IConfiguration configuration; + private readonly ConcurrentDictionary store = new ConcurrentDictionary(); + + public ServiceManagerStore(ServiceTransportType transportType, IConfiguration configuration, ILoggerFactory loggerFactory) + { + this.loggerFactory = loggerFactory; + this.transportType = transportType; + this.configuration = configuration; + } + + public IServiceHubContextStore GetOrAddByConfigurationKey(string configurationKey) + { + string connectionString = configuration[configurationKey]; + return GetOrAddByConnectionString(connectionString); + } + + public IServiceHubContextStore GetOrAddByConnectionString(string connectionString) + { + return store.GetOrAdd(connectionString, CreateHubContextStore); + } + + // test only + public IServiceHubContextStore GetByConfigurationKey(string configurationKey) + { + string connectionString = configuration[configurationKey]; + return store.ContainsKey(connectionString) ? store[connectionString] : null; + } + + private IServiceHubContextStore CreateHubContextStore(string connectionString) + { + var serviceManager = CreateServiceManager(connectionString); + return new ServiceHubContextStore(serviceManager, loggerFactory); + } + + private IServiceManager CreateServiceManager(string connectionString) + { + return new ServiceManagerBuilder().WithOptions(o => + { + o.ConnectionString = connectionString; + o.ServiceTransportType = transportType; + }).Build(); + } + } +} diff --git a/src/SignalRServiceExtension/Config/SignalRConfigProvider.cs b/src/SignalRServiceExtension/Config/SignalRConfigProvider.cs index bf095c07..bf0143fd 100644 --- a/src/SignalRServiceExtension/Config/SignalRConfigProvider.cs +++ b/src/SignalRServiceExtension/Config/SignalRConfigProvider.cs @@ -1,28 +1,25 @@ // 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.Azure.SignalR.Management; using Microsoft.Azure.WebJobs.Description; using Microsoft.Azure.WebJobs.Host.Bindings; using Microsoft.Azure.WebJobs.Host.Config; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; namespace Microsoft.Azure.WebJobs.Extensions.SignalRService { [Extension("SignalR")] internal class SignalRConfigProvider : IExtensionConfigProvider { - internal const string AzureSignalRConnectionStringName = "AzureSignalRConnectionString"; - private const string ServiceTransportTypeName = "AzureSignalRServiceTransportType"; - private static IServiceManager serviceManager; + public IConfiguration Configuration { get; } + private readonly SignalROptions options; private readonly INameResolver nameResolver; private readonly ILogger logger; @@ -31,12 +28,14 @@ internal class SignalRConfigProvider : IExtensionConfigProvider public SignalRConfigProvider( IOptions options, INameResolver nameResolver, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + IConfiguration configuration) { this.options = options.Value; this.loggerFactory = loggerFactory; this.logger = loggerFactory.CreateLogger("SignalR"); this.nameResolver = nameResolver; + Configuration = configuration; } public void Initialize(ExtensionConfigContext context) @@ -48,10 +47,10 @@ public void Initialize(ExtensionConfigContext context) if (string.IsNullOrEmpty(options.ConnectionString)) { - options.ConnectionString = nameResolver.Resolve(AzureSignalRConnectionStringName); + options.ConnectionString = nameResolver.Resolve(Constants.AzureSignalRConnectionStringName); } - var serviceTransportTypeStr = nameResolver.Resolve(ServiceTransportTypeName); + var serviceTransportTypeStr = nameResolver.Resolve(Constants.ServiceTransportTypeName); if (Enum.TryParse(serviceTransportTypeStr, out var transport)) { options.AzureSignalRServiceTransportType = transport; @@ -61,12 +60,7 @@ public void Initialize(ExtensionConfigContext context) logger.LogWarning($"Unsupported service transport type: {serviceTransportTypeStr}. Use default {options.AzureSignalRServiceTransportType} instead."); } - serviceManager = new ServiceManagerBuilder().WithOptions(o => - { - o.ConnectionString = options.ConnectionString; - o.ServiceTransportType = options.AzureSignalRServiceTransportType; - }).Build(); - StaticServiceHubContextStore.ServiceHubContextStore = new ServiceHubContextStore(serviceManager, loggerFactory); + StaticServiceHubContextStore.ServiceManagerStore = new ServiceManagerStore(options.AzureSignalRServiceTransportType, Configuration, loggerFactory); context.AddConverter(JObject.FromObject) .AddConverter(JObject.FromObject) @@ -84,6 +78,14 @@ public void Initialize(ExtensionConfigContext context) logger.LogInformation("SignalRService binding initialized"); } + public AzureSignalRClient GetAzureSignalRClient(string attributeConnectionString, string attributeHubName) + { + var connectionString = FirstOrDefault(attributeConnectionString, options.ConnectionString); + var hubName = FirstOrDefault(attributeHubName, options.HubName); + + return new AzureSignalRClient(StaticServiceHubContextStore.ServiceManagerStore, connectionString, hubName); + } + private void ValidateSignalRAttributeBinding(SignalRAttribute attribute, Type type) { ValidateConnectionString( @@ -104,15 +106,14 @@ private void ValidateConnectionString(string attributeConnectionString, string a if (string.IsNullOrEmpty(connectionString)) { - throw new InvalidOperationException( - $"The SignalR Service connection string must be set either via an '{AzureSignalRConnectionStringName}' app setting, via an '{AzureSignalRConnectionStringName}' environment variable, or directly in code via {nameof(SignalROptions)}.{nameof(SignalROptions.ConnectionString)} or {attributeConnectionStringName}."); + throw new InvalidOperationException(string.Format(ErrorMessages.EmptyConnectionStringErrorMessageFormat, attributeConnectionStringName)); } } private SignalRConnectionInfo GetClientConnectionInfo(SignalRConnectionInfoAttribute attribute) { - var signalR = new AzureSignalRClient(StaticServiceHubContextStore.ServiceHubContextStore, serviceManager); - return signalR.GetClientConnectionInfo(attribute.HubName, attribute.UserId, attribute.IdToken, attribute.ClaimTypeList); + var client = GetAzureSignalRClient(attribute.ConnectionStringSetting, attribute.HubName); + return client.GetClientConnectionInfo(attribute.UserId, attribute.IdToken, attribute.ClaimTypeList); } private string FirstOrDefault(params string[] values) @@ -120,13 +121,6 @@ private string FirstOrDefault(params string[] values) return values.FirstOrDefault(v => !string.IsNullOrEmpty(v)); } - internal AzureSignalRClient GetClient(SignalRAttribute attribute) - { - var connectionString = FirstOrDefault(attribute.ConnectionStringSetting, options.ConnectionString); - var hubName = FirstOrDefault(attribute.HubName, options.HubName); - return new AzureSignalRClient(StaticServiceHubContextStore.ServiceHubContextStore, serviceManager); - } - private class SignalROpenType : OpenType.Poco { public override bool IsMatch(Type type, OpenTypeMatchContext context) diff --git a/src/SignalRServiceExtension/Config/StaticServiceHubContextStore.cs b/src/SignalRServiceExtension/Config/StaticServiceHubContextStore.cs index ac17a9ac..bc67514f 100644 --- a/src/SignalRServiceExtension/Config/StaticServiceHubContextStore.cs +++ b/src/SignalRServiceExtension/Config/StaticServiceHubContextStore.cs @@ -1,27 +1,24 @@ // 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; -using Microsoft.Azure.SignalR.Management; - namespace Microsoft.Azure.WebJobs.Extensions.SignalRService { /// - /// A global for the extension. - /// It stores per hub. + /// A global for the extension. + /// It stores per connection string. /// public static class StaticServiceHubContextStore { /// - /// Gets or adds . - /// If the for a specific hub exists, returns the , + /// Gets . + /// If the for a specific connection string exists, returns the , /// otherwise creates one and then returns it. /// - /// Hub name of the - /// which is a context abstraction for a hub. - public static ValueTask GetOrAddAsync(string hubName) => - ServiceHubContextStore.GetOrAddAsync(hubName); + /// is the connection string configuration key. + /// The returned value is an instance of . + public static IServiceHubContextStore Get(string configurationKey = Constants.AzureSignalRConnectionStringName) => + ServiceManagerStore.GetOrAddByConfigurationKey(configurationKey); - internal static IServiceHubContextStore ServiceHubContextStore { get; set; } + internal static IServiceManagerStore ServiceManagerStore { get; set; } } } diff --git a/src/SignalRServiceExtension/Constants.cs b/src/SignalRServiceExtension/Constants.cs new file mode 100644 index 00000000..2e79b820 --- /dev/null +++ b/src/SignalRServiceExtension/Constants.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. + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService +{ + internal static class Constants + { + public const string AzureSignalRConnectionStringName = "AzureSignalRConnectionString"; + public const string ServiceTransportTypeName = "AzureSignalRServiceTransportType"; + } +} diff --git a/src/SignalRServiceExtension/ErrorMessages.cs b/src/SignalRServiceExtension/ErrorMessages.cs new file mode 100644 index 00000000..1c7d623c --- /dev/null +++ b/src/SignalRServiceExtension/ErrorMessages.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. + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService +{ + internal static class ErrorMessages + { + public static readonly string EmptyConnectionStringErrorMessageFormat = + $"The SignalR Service connection string must be set either via an '{Constants.AzureSignalRConnectionStringName}' app setting, via an '{Constants.AzureSignalRConnectionStringName}' environment variable, or directly in code via {nameof(SignalROptions)}.{nameof(SignalROptions.ConnectionString)} or {{0}}."; + } +} diff --git a/src/SignalRServiceExtension/SignalRAttribute.cs b/src/SignalRServiceExtension/SignalRAttribute.cs index 4139378e..2eded757 100644 --- a/src/SignalRServiceExtension/SignalRAttribute.cs +++ b/src/SignalRServiceExtension/SignalRAttribute.cs @@ -11,7 +11,7 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService public class SignalRAttribute : Attribute { - [AppSetting(Default = SignalRConfigProvider.AzureSignalRConnectionStringName)] + [AppSetting(Default = Constants.AzureSignalRConnectionStringName)] public string ConnectionStringSetting { get; set; } [AutoResolve] diff --git a/src/SignalRServiceExtension/SignalRConnectionInfoAttribute.cs b/src/SignalRServiceExtension/SignalRConnectionInfoAttribute.cs index d41ead31..fd9c111a 100644 --- a/src/SignalRServiceExtension/SignalRConnectionInfoAttribute.cs +++ b/src/SignalRServiceExtension/SignalRConnectionInfoAttribute.cs @@ -12,7 +12,7 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService [Binding] public class SignalRConnectionInfoAttribute : Attribute { - [AppSetting(Default = SignalRConfigProvider.AzureSignalRConnectionStringName)] + [AppSetting(Default = Constants.AzureSignalRConnectionStringName)] public string ConnectionStringSetting { get; set; } [AutoResolve] diff --git a/test/SignalRServiceExtension.Tests/AzureSignalRClientTests.cs b/test/SignalRServiceExtension.Tests/AzureSignalRClientTests.cs index 6c59d029..53062bb8 100644 --- a/test/SignalRServiceExtension.Tests/AzureSignalRClientTests.cs +++ b/test/SignalRServiceExtension.Tests/AzureSignalRClientTests.cs @@ -25,15 +25,9 @@ public void GetClientConnectionInfo() var expectedName = "John Doe"; var expectedIat = "1516239022"; var claimTypeList = new string[] { "name", "iat" }; - var serviceManager = new ServiceManagerBuilder() - .WithOptions(o => - { - o.ConnectionString = connectionString; - }) - .Build(); - var serviceHubContextStore = new ServiceHubContextStore(serviceManager, null); - var azureSignalRClient = new AzureSignalRClient(serviceHubContextStore, serviceManager); - var connectionInfo = azureSignalRClient.GetClientConnectionInfo(hubName, userId, idToken, claimTypeList); + var serviceManagerStore = new ServiceManagerStore(ServiceTransportType.Transient, null, null); + var azureSignalRClient = new AzureSignalRClient(serviceManagerStore, connectionString, hubName); + var connectionInfo = azureSignalRClient.GetClientConnectionInfo(userId, idToken, claimTypeList); Assert.Equal(connectionInfo.Url, $"{hubUrl}/client/?hub={hubName.ToLower()}"); diff --git a/test/SignalRServiceExtension.Tests/JobhostEndToEnd.cs b/test/SignalRServiceExtension.Tests/JobhostEndToEnd.cs new file mode 100644 index 00000000..28f51a94 --- /dev/null +++ b/test/SignalRServiceExtension.Tests/JobhostEndToEnd.cs @@ -0,0 +1,243 @@ +// 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.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.SignalRService; +using Microsoft.Azure.WebJobs.Host.Indexers; +using Microsoft.IdentityModel.Tokens; +using SignalRServiceExtension.Tests.Utils; +using SignalRServiceExtension.Tests.Utils.Loggings; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace SignalRServiceExtension.Tests +{ + public class JobhostEndToEnd + { + private const string AttrConnStrConfigKey = "AttributeConnectionStringName"; + private const string DefaultUserId = "UserId"; + private const string DefaultHubName = "TestHub"; + private const string DefaultEndpoint = "http://abc.com"; + private const string DefaultAccessKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + private const string DefaultAttributeAccessKey = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + private const string DefaultConnectionStringFormat = "Endpoint={0};AccessKey={1};Version=1.0;"; + private static readonly string DefaultConnectionString = string.Format(DefaultConnectionStringFormat, DefaultEndpoint, DefaultAccessKey); + private static readonly string DefaultAttributeConnectionString = string.Format(DefaultConnectionStringFormat, DefaultEndpoint, DefaultAttributeAccessKey); + private static Dictionary _curConfigDict; + private readonly ITestOutputHelper _output; + + public static Dictionary ConnStrInsideOfAttrConfigDict = new Dictionary + { + [AttrConnStrConfigKey] = DefaultAttributeConnectionString, + }; + + public static Dictionary ConnStrOutsideOfAttrConfigDict = new Dictionary + { + [Constants.AzureSignalRConnectionStringName] = DefaultConnectionString, + }; + + public static Dictionary DiffConfigKeySameConnStrConfigDict = new Dictionary + { + [AttrConnStrConfigKey] = DefaultConnectionString, + [Constants.AzureSignalRConnectionStringName] = DefaultConnectionString + }; + + public static Dictionary DiffConfigKeyDiffConnStrConfigDict = new Dictionary + { + [AttrConnStrConfigKey] = DefaultAttributeConnectionString, + [Constants.AzureSignalRConnectionStringName] = DefaultConnectionString + }; + + public static Dictionary[] TestConfigDicts = { + ConnStrInsideOfAttrConfigDict, + ConnStrOutsideOfAttrConfigDict, + DiffConfigKeySameConnStrConfigDict, + DiffConfigKeyDiffConnStrConfigDict, + null, + DiffConfigKeyDiffConnStrConfigDict, + }; + + public static Type[] TestClassTypesForSignalRAttribute = + { + typeof(SignalRFunctionsWithConnectionString), + typeof(SignalRFunctionsWithoutConnectionString), + typeof(SignalRFunctionsWithConnectionString), + typeof(SignalRFunctionsWithConnectionString), + typeof(SignalRFunctionsWithoutConnectionString), + typeof(SignalRFunctionsWithMultipleConnectionStrings), + }; + + public static Type[] TestClassTypesForSignalRConnectionInfoAttribute = + { + typeof(SignalRConnectionInfoFunctionsWithConnectionString), + typeof(SignalRConnectionInfoFunctionsWithoutConnectionString), + typeof(SignalRConnectionInfoFunctionsWithConnectionString), + typeof(SignalRConnectionInfoFunctionsWithConnectionString), + typeof(SignalRConnectionInfoFunctionsWithoutConnectionString), + typeof(SignalRConnectionInfoFunctionsWithMultipleConnectionStrings), + }; + + public static IEnumerable SignalRAttributeTestData => GenerateTestData(TestClassTypesForSignalRAttribute, TestConfigDicts, GenerateTestExpectedErrorMessages($"{nameof(SignalRAttribute)}.{nameof(SignalRAttribute.ConnectionStringSetting)}")); + + public static IEnumerable SignalRConnectionInfoAttributeTestData => GenerateTestData(TestClassTypesForSignalRConnectionInfoAttribute, TestConfigDicts, GenerateTestExpectedErrorMessages($"{nameof(SignalRConnectionInfoAttribute)}.{nameof(SignalRConnectionInfoAttribute.ConnectionStringSetting)}")); + + public JobhostEndToEnd(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [MemberData(nameof(SignalRAttributeTestData))] + [MemberData(nameof(SignalRConnectionInfoAttributeTestData))] + public async Task ConnectionStringSettingFacts(Type classType, Dictionary configDict, string expectedErrorMessage) + { + if (configDict != null) + { + configDict[Constants.ServiceTransportTypeName] = nameof(ServiceTransportType.Transient); + } + _curConfigDict = configDict; + var host = TestHelpers.NewHost(classType, configuration: configDict, loggerProvider: new XunitLoggerProvider(_output)); + if (expectedErrorMessage == null) + { + await Task.WhenAll(from method in classType.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) + select host.GetJobHost().CallAsync($"{classType.Name}.{method.Name}")); + } + else + { + var indexException = await Assert.ThrowsAsync(() => host.StartAsync()); + Assert.Equal(expectedErrorMessage, indexException.InnerException.Message); + } + } + + public static string[] GenerateTestExpectedErrorMessages(string attributePropertyName) => new string[] + { + null, + null, + null, + null, + string.Format(ErrorMessages.EmptyConnectionStringErrorMessageFormat, attributePropertyName), + null, + }; + + public static IEnumerable GenerateTestData(Type[] classType, Dictionary[] configDicts, string[] expectedErrorMessages) + { + if (classType.Length != expectedErrorMessages.Length || classType.Length != configDicts.Length) + { + throw new ArgumentException($"Length of {nameof(classType)}, {nameof(configDicts)} and {nameof(expectedErrorMessages)} are not the same."); + } + for (var i = 0; i < expectedErrorMessages.Length; i++) + { + yield return new object[] { classType[i], configDicts[i], expectedErrorMessages[i] }; + } + } + + private static void UpdateFunctionOutConnectionString(SignalRConnectionInfo connectionInfo, string expectedConfigurationKey) + { + var handler = new JwtSecurityTokenHandler(); + var accessKeys = new List { DefaultAccessKey, DefaultAttributeAccessKey }; + var validationParameters = new TokenValidationParameters(); + validationParameters.ValidateIssuer = false; + validationParameters.ValidateAudience = false; + validationParameters.IssuerSigningKeyResolver = (token, securityToken, kid, validationParas) => from key in accessKeys + select new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); + handler.ValidateToken(connectionInfo.AccessToken, validationParameters, out var validatedToken); + var validatedAccessKey = Encoding.UTF8.GetString((validatedToken.SigningKey as SymmetricSecurityKey)?.Key); + var actualConnectionString = string.Format(DefaultConnectionStringFormat, DefaultEndpoint, validatedAccessKey); + Assert.Equal(_curConfigDict[expectedConfigurationKey], actualConnectionString); + } + + private static async Task SimulateSendingMessage(IAsyncCollector signalRMessages) + { + try + { + await signalRMessages.AddAsync( + new SignalRMessage + { + UserId = DefaultUserId, + GroupName = "", + Target = "newMessage", + Arguments = new[] { "message" } + }); + } + catch + { + // ignore all the exception, since we only want to test whether the service manager for specific is added in the service manager store + } + } + + #region SignalRAttributeTests + public class SignalRFunctionsWithConnectionString + { + public async Task Func([SignalR(HubName = DefaultHubName, ConnectionStringSetting = AttrConnStrConfigKey)] IAsyncCollector signalRMessages) + { + await SimulateSendingMessage(signalRMessages); + Assert.NotNull(((ServiceManagerStore)StaticServiceHubContextStore.ServiceManagerStore).GetByConfigurationKey(AttrConnStrConfigKey)); + } + } + + public class SignalRFunctionsWithoutConnectionString + { + public async Task Func([SignalR(HubName = DefaultHubName)] IAsyncCollector signalRMessages) + { + await SimulateSendingMessage(signalRMessages); + Assert.NotNull(((ServiceManagerStore)StaticServiceHubContextStore.ServiceManagerStore).GetByConfigurationKey(Constants.AzureSignalRConnectionStringName)); + } + } + + public class SignalRFunctionsWithMultipleConnectionStrings + { + public async Task Func1([SignalR(HubName = DefaultHubName, ConnectionStringSetting = Constants.AzureSignalRConnectionStringName)] IAsyncCollector signalRMessages) + { + await SimulateSendingMessage(signalRMessages); + Assert.NotNull(((ServiceManagerStore)StaticServiceHubContextStore.ServiceManagerStore).GetByConfigurationKey(Constants.AzureSignalRConnectionStringName)); + } + + public async Task Func2([SignalR(HubName = DefaultHubName, ConnectionStringSetting = AttrConnStrConfigKey)] IAsyncCollector signalRMessages) + { + await SimulateSendingMessage(signalRMessages); + Assert.NotNull(((ServiceManagerStore)StaticServiceHubContextStore.ServiceManagerStore).GetByConfigurationKey(AttrConnStrConfigKey)); + } + } + #endregion + + #region SignalRConnectionInfoAttributeTests + public class SignalRConnectionInfoFunctionsWithConnectionString + { + public void Func([SignalRConnectionInfo(UserId = DefaultUserId, HubName = DefaultHubName, ConnectionStringSetting = AttrConnStrConfigKey)] SignalRConnectionInfo connectionInfo) + { + UpdateFunctionOutConnectionString(connectionInfo, AttrConnStrConfigKey); + } + } + + public class SignalRConnectionInfoFunctionsWithoutConnectionString + { + public void Func([SignalRConnectionInfo(UserId = DefaultUserId, HubName = DefaultHubName)] SignalRConnectionInfo connectionInfo) + { + UpdateFunctionOutConnectionString(connectionInfo, Constants.AzureSignalRConnectionStringName); + } + } + + public class SignalRConnectionInfoFunctionsWithMultipleConnectionStrings + { + public void Func1([SignalRConnectionInfo(UserId = DefaultUserId, HubName = DefaultHubName, ConnectionStringSetting = Constants.AzureSignalRConnectionStringName)] SignalRConnectionInfo connectionInfo) + { + UpdateFunctionOutConnectionString(connectionInfo, Constants.AzureSignalRConnectionStringName); + } + + public void Func2([SignalRConnectionInfo(UserId = DefaultUserId, HubName = DefaultHubName, ConnectionStringSetting = AttrConnStrConfigKey)] SignalRConnectionInfo connectionInfo) + { + UpdateFunctionOutConnectionString(connectionInfo, AttrConnStrConfigKey); + } + } + #endregion + } +} diff --git a/test/SignalRServiceExtension.Tests/SignalRAsyncCollectorTests.cs b/test/SignalRServiceExtension.Tests/SignalRAsyncCollectorTests.cs index e429ce45..255ec57b 100644 --- a/test/SignalRServiceExtension.Tests/SignalRAsyncCollectorTests.cs +++ b/test/SignalRServiceExtension.Tests/SignalRAsyncCollectorTests.cs @@ -11,11 +11,13 @@ namespace SignalRServiceExtension.Tests { public class SignalRAsyncCollectorTests { + private const string DefaultHubName = "chathub"; + [Fact] public async Task AddAsync_WithBroadcastMessage_CallsSendToAll() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRMessage { @@ -23,9 +25,9 @@ await collector.AddAsync(new SignalRMessage Arguments = new object[] { "arg1", "arg2" } }); - signalRSenderMock.Verify(c => c.SendToAll("chathub", It.IsAny()), Times.Once); + signalRSenderMock.Verify(c => c.SendToAll(It.IsAny()), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); - var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[1]; + var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[0]; Assert.Equal("newMessage", actualData.Target); Assert.Equal("arg1", actualData.Arguments[0]); Assert.Equal("arg2", actualData.Arguments[1]); @@ -35,7 +37,7 @@ await collector.AddAsync(new SignalRMessage public async Task AddAsync_WithUserId_CallsSendToUser() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRMessage { @@ -45,10 +47,10 @@ await collector.AddAsync(new SignalRMessage }); signalRSenderMock.Verify( - c => c.SendToUser("chathub", "userId1", It.IsAny()), + c => c.SendToUser("userId1", It.IsAny()), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); - var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[2]; + var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[1]; Assert.Equal("newMessage", actualData.Target); Assert.Equal("arg1", actualData.Arguments[0]); Assert.Equal("arg2", actualData.Arguments[1]); @@ -58,7 +60,7 @@ await collector.AddAsync(new SignalRMessage public async Task AddAsync_WithUserId_CallsSendToGroup() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRMessage { @@ -68,10 +70,10 @@ await collector.AddAsync(new SignalRMessage }); signalRSenderMock.Verify( - c => c.SendToGroup("chathub", "group1", It.IsAny()), + c => c.SendToGroup("group1", It.IsAny()), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); - var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[2]; + var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[1]; Assert.Equal("newMessage", actualData.Target); Assert.Equal("arg1", actualData.Arguments[0]); Assert.Equal("arg2", actualData.Arguments[1]); @@ -81,7 +83,7 @@ await collector.AddAsync(new SignalRMessage public async Task AddAsync_WithUserId_CallsAddUserToGroup() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRGroupAction { @@ -91,20 +93,19 @@ await collector.AddAsync(new SignalRGroupAction }); signalRSenderMock.Verify( - c => c.AddUserToGroup("chathub", "userId1", "group1"), + c => c.AddUserToGroup("userId1", "group1"), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); var actualData = signalRSenderMock.Invocations[0]; - Assert.Equal("chathub", actualData.Arguments[0]); - Assert.Equal("userId1", actualData.Arguments[1]); - Assert.Equal("group1", actualData.Arguments[2]); + Assert.Equal("userId1", actualData.Arguments[0]); + Assert.Equal("group1", actualData.Arguments[1]); } [Fact] public async Task AddAsync_WithUserId_CallsRemoveUserFromGroup() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRGroupAction { @@ -114,20 +115,19 @@ await collector.AddAsync(new SignalRGroupAction }); signalRSenderMock.Verify( - c => c.RemoveUserFromGroup("chathub", "userId1", "group1"), + c => c.RemoveUserFromGroup("userId1", "group1"), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); var actualData = signalRSenderMock.Invocations[0]; - Assert.Equal("chathub", actualData.Arguments[0]); - Assert.Equal("userId1", actualData.Arguments[1]); - Assert.Equal("group1", actualData.Arguments[2]); + Assert.Equal("userId1", actualData.Arguments[0]); + Assert.Equal("group1", actualData.Arguments[1]); } [Fact] public async Task AddAsync_InvalidTypeThrowException() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); var item = new object[] { "arg1", "arg2" }; @@ -138,7 +138,7 @@ public async Task AddAsync_InvalidTypeThrowException() public async Task AddAsync_SendMessage_WithBothUserIdAndGroupName_UsePriorityOrder() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRMessage { @@ -149,10 +149,10 @@ await collector.AddAsync(new SignalRMessage }); signalRSenderMock.Verify( - c => c.SendToUser("chathub", "user1", It.IsAny()), + c => c.SendToUser("user1", It.IsAny()), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); - var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[2]; + var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[1]; Assert.Equal("newMessage", actualData.Target); Assert.Equal("arg1", actualData.Arguments[0]); Assert.Equal("arg2", actualData.Arguments[1]); @@ -162,7 +162,7 @@ await collector.AddAsync(new SignalRMessage public async Task AddAsync_WithConnectionId_CallsSendToUser() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRMessage { @@ -172,10 +172,10 @@ await collector.AddAsync(new SignalRMessage }); signalRSenderMock.Verify( - c => c.SendToConnection("chathub", "connection1", It.IsAny()), + c => c.SendToConnection("connection1", It.IsAny()), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); - var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[2]; + var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[1]; Assert.Equal("newMessage", actualData.Target); Assert.Equal("arg1", actualData.Arguments[0]); Assert.Equal("arg2", actualData.Arguments[1]); @@ -185,7 +185,7 @@ await collector.AddAsync(new SignalRMessage public async Task AddAsync_WithConnectionId_CallsAddConnectionToGroup() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRGroupAction { @@ -195,20 +195,19 @@ await collector.AddAsync(new SignalRGroupAction }); signalRSenderMock.Verify( - c => c.AddConnectionToGroup("chathub", "connection1", "group1"), + c => c.AddConnectionToGroup("connection1", "group1"), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); var actualData = signalRSenderMock.Invocations[0]; - Assert.Equal("chathub", actualData.Arguments[0]); - Assert.Equal("connection1", actualData.Arguments[1]); - Assert.Equal("group1", actualData.Arguments[2]); + Assert.Equal("connection1", actualData.Arguments[0]); + Assert.Equal("group1", actualData.Arguments[1]); } [Fact] public async Task AddAsync_WithConnectionId_CallsRemoveConnectionFromGroup() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); await collector.AddAsync(new SignalRGroupAction { @@ -218,20 +217,19 @@ await collector.AddAsync(new SignalRGroupAction }); signalRSenderMock.Verify( - c => c.RemoveConnectionFromGroup("chathub", "connection1", "group1"), + c => c.RemoveConnectionFromGroup("connection1", "group1"), Times.Once); signalRSenderMock.VerifyNoOtherCalls(); var actualData = signalRSenderMock.Invocations[0]; - Assert.Equal("chathub", actualData.Arguments[0]); - Assert.Equal("connection1", actualData.Arguments[1]); - Assert.Equal("group1", actualData.Arguments[2]); + Assert.Equal("connection1", actualData.Arguments[0]); + Assert.Equal("group1", actualData.Arguments[1]); } [Fact] public async Task AddAsync_GroupOperation_WithoutParametersThrowException() { var signalRSenderMock = new Mock(); - var collector = new SignalRAsyncCollector(signalRSenderMock.Object, "chathub"); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); var item = new SignalRGroupAction { diff --git a/test/SignalRServiceExtension.Tests/Utils/FakeTypeLocator.cs b/test/SignalRServiceExtension.Tests/Utils/FakeTypeLocator.cs new file mode 100644 index 00000000..853f5258 --- /dev/null +++ b/test/SignalRServiceExtension.Tests/Utils/FakeTypeLocator.cs @@ -0,0 +1,24 @@ +// 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 Microsoft.Azure.WebJobs; + +namespace SignalRServiceExtension.Tests.Utils +{ + public class FakeTypeLocator : ITypeLocator + { + private Type _type; + + public FakeTypeLocator(Type type) + { + _type = type; + } + + public IReadOnlyList GetTypes() + { + return new Type[] { _type }; + } + } +} \ No newline at end of file diff --git a/test/SignalRServiceExtension.Tests/Utils/Loggings/XunitLogger.cs b/test/SignalRServiceExtension.Tests/Utils/Loggings/XunitLogger.cs new file mode 100644 index 00000000..198f8357 --- /dev/null +++ b/test/SignalRServiceExtension.Tests/Utils/Loggings/XunitLogger.cs @@ -0,0 +1,41 @@ +// 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.Extensions.Logging; +using Xunit.Abstractions; + +namespace SignalRServiceExtension.Tests.Utils.Loggings +{ + internal class XunitLogger : ILogger + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly string _categoryName; + + public XunitLogger(ITestOutputHelper testOutputHelper, string categoryName) + { + _testOutputHelper = testOutputHelper; + _categoryName = categoryName; + } + + public IDisposable BeginScope(TState state) + => NoopDisposable.Instance; + + public bool IsEnabled(LogLevel logLevel) + => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _testOutputHelper.WriteLine($"{_categoryName} [{eventId}] {formatter(state, exception)}"); + if (exception != null) + _testOutputHelper.WriteLine(exception.ToString()); + } + + private class NoopDisposable : IDisposable + { + public static NoopDisposable Instance = new NoopDisposable(); + public void Dispose() + { } + } + } +} diff --git a/test/SignalRServiceExtension.Tests/Utils/Loggings/XunitLoggerProvider.cs b/test/SignalRServiceExtension.Tests/Utils/Loggings/XunitLoggerProvider.cs new file mode 100644 index 00000000..a566f565 --- /dev/null +++ b/test/SignalRServiceExtension.Tests/Utils/Loggings/XunitLoggerProvider.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace SignalRServiceExtension.Tests.Utils.Loggings +{ + internal class XunitLoggerProvider : ILoggerProvider + { + private readonly ITestOutputHelper _testOutputHelper; + + public XunitLoggerProvider(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + public ILogger CreateLogger(string categoryName) + => new XunitLogger(_testOutputHelper, categoryName); + + public void Dispose() + { } + } +} diff --git a/test/SignalRServiceExtension.Tests/Utils/TestExtensionConfig.cs b/test/SignalRServiceExtension.Tests/Utils/TestExtensionConfig.cs new file mode 100644 index 00000000..4c96ca88 --- /dev/null +++ b/test/SignalRServiceExtension.Tests/Utils/TestExtensionConfig.cs @@ -0,0 +1,31 @@ +// 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.Azure.WebJobs.Description; +using Microsoft.Azure.WebJobs.Host.Config; + +namespace SignalRServiceExtension.Tests.Utils +{ + public class TestExtensionConfig : IExtensionConfigProvider + { + // todo: what's this + [Binding] + public class BindingDataAttribute : Attribute + { + public BindingDataAttribute(string toBeAutoResolve) + { + ToBeAutoResolve = toBeAutoResolve; + } + + [AutoResolve] + public string ToBeAutoResolve { get; set; } + } + + public void Initialize(ExtensionConfigContext context) + { + context.AddBindingRule(). + BindToInput(attr => attr.ToBeAutoResolve); + } + } +} diff --git a/test/SignalRServiceExtension.Tests/Utils/TestHelpers.cs b/test/SignalRServiceExtension.Tests/Utils/TestHelpers.cs index bb060045..02d7d7b1 100644 --- a/test/SignalRServiceExtension.Tests/Utils/TestHelpers.cs +++ b/test/SignalRServiceExtension.Tests/Utils/TestHelpers.cs @@ -2,32 +2,56 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using Microsoft.IdentityModel.Tokens; +using System.Collections.Generic; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.SignalRService; +using Microsoft.Azure.WebJobs.Host.Config; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace SignalRServiceExtension.Tests.Utils { - class TestHelpers + internal static class TestHelpers { - internal static ClaimsPrincipal EnsureValidAccessToken(string audience, string signingKey, string accessToken) + public static IHost NewHost(Type type, SignalRConfigProvider ext = null, Dictionary configuration = null, ILoggerProvider loggerProvider = null) { - var validationParameters = - new TokenValidationParameters + var builder = new HostBuilder() + .ConfigureServices(services => { - ValidAudiences = new[] { audience }, - ValidateAudience = true, - ValidateIssuer = false, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)), - RequireSignedTokens = true, - RequireExpirationTime = true, - ValidateLifetime = true, - LifetimeValidator = (_, expires, __, ___) => - expires.HasValue && expires > DateTime.UtcNow.AddMinutes(5) // at least 5 minutes - }; - var handler = new JwtSecurityTokenHandler(); - return handler.ValidateToken(accessToken, validationParameters, out _); + services.AddSingleton(new FakeTypeLocator(type)); + if (ext != null) + { + services.AddSingleton(ext); + } + services.AddSingleton(new TestExtensionConfig()); + }) + .ConfigureWebJobs(webJobsBuilder => + { + webJobsBuilder.AddSignalR(); + webJobsBuilder.UseHostId(Guid.NewGuid().ToString("n")); + }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddProvider(loggerProvider); + }); + + if (configuration != null) + { + builder.ConfigureAppConfiguration(b => + { + b.AddInMemoryCollection(configuration); + }); + } + + return builder.Build(); + } + + public static JobHost GetJobHost(this IHost host) + { + return host.Services.GetService() as JobHost; } } } \ No newline at end of file From 5700e16515e4399764aa3d6fbe46a504fd2cbab8 Mon Sep 17 00:00:00 2001 From: JialinXin Date: Tue, 8 Oct 2019 12:56:22 +0800 Subject: [PATCH 2/3] Update version to 1.0.2 (#78) * add java gitignore * exclude Java IDEs * add java library * install mvn in travis * update maven install * fix paths * remove unused plugins and version assign * uniform spaces in pom * Update version to 1.0.2 --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index fbc0026e..6a595005 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 1.0.1 + 1.0.2 preview1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final From a471f3d1f76d1f5c6fcd090c876fcd2223893934 Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 12 Oct 2019 00:00:50 -0500 Subject: [PATCH 3/3] Remove user from all groups (#76) * Remove user from all groups * Rename enum member for RemoveAll from 'remove-all' to 'removeAll' --- .../Bindings/SignalRAsyncCollector.cs | 29 +++++++++++-------- .../Bindings/SignalRGroupAction.cs | 4 ++- .../Client/AzureSignalRClient.cs | 10 +++++++ .../Client/IAzureSignalRSender.cs | 1 + .../SignalRAsyncCollectorTests.cs | 20 +++++++++++++ 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/SignalRServiceExtension/Bindings/SignalRAsyncCollector.cs b/src/SignalRServiceExtension/Bindings/SignalRAsyncCollector.cs index 92f3183b..9f9687eb 100644 --- a/src/SignalRServiceExtension/Bindings/SignalRAsyncCollector.cs +++ b/src/SignalRServiceExtension/Bindings/SignalRAsyncCollector.cs @@ -59,24 +59,29 @@ internal SignalRAsyncCollector(IAzureSignalRSender client) if (!string.IsNullOrEmpty(groupAction.ConnectionId)) { - if (groupAction.Action == GroupAction.Add) + switch(groupAction.Action) { - await client.AddConnectionToGroup(groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false); - } - else - { - await client.RemoveConnectionFromGroup(groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false); + case GroupAction.Add: + await client.AddConnectionToGroup(groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false); + break; + case GroupAction.Remove: + await client.RemoveConnectionFromGroup(groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false); + break; } } else if (!string.IsNullOrEmpty(groupAction.UserId)) { - if (groupAction.Action == GroupAction.Add) - { - await client.AddUserToGroup(groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); - } - else + switch (groupAction.Action) { - await client.RemoveUserFromGroup(groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); + case GroupAction.Add: + await client.AddUserToGroup(groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); + break; + case GroupAction.Remove: + await client.RemoveUserFromGroup(groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); + break; + case GroupAction.RemoveAll: + await client.RemoveUserFromAllGroups(groupAction.UserId).ConfigureAwait(false); + break; } } else diff --git a/src/SignalRServiceExtension/Bindings/SignalRGroupAction.cs b/src/SignalRServiceExtension/Bindings/SignalRGroupAction.cs index dff7b900..1bf611bf 100644 --- a/src/SignalRServiceExtension/Bindings/SignalRGroupAction.cs +++ b/src/SignalRServiceExtension/Bindings/SignalRGroupAction.cs @@ -33,6 +33,8 @@ public enum GroupAction [EnumMember(Value = "add")] Add, [EnumMember(Value = "remove")] - Remove + Remove, + [EnumMember(Value = "removeAll")] + RemoveAll } } \ No newline at end of file diff --git a/src/SignalRServiceExtension/Client/AzureSignalRClient.cs b/src/SignalRServiceExtension/Client/AzureSignalRClient.cs index ec062216..fdd0a587 100644 --- a/src/SignalRServiceExtension/Client/AzureSignalRClient.cs +++ b/src/SignalRServiceExtension/Client/AzureSignalRClient.cs @@ -117,6 +117,16 @@ public async Task RemoveUserFromGroup(string userId, string groupName) await serviceHubContext.UserGroups.RemoveFromGroupAsync(userId, groupName); } + public async Task RemoveUserFromAllGroups(string userId) + { + if (string.IsNullOrEmpty(userId)) + { + throw new ArgumentException($"{nameof(userId)} cannot be null or empty"); + } + var serviceHubContext = await serviceManagerStore.GetOrAddByConnectionString(connectionString).GetAsync(hubName); + await serviceHubContext.UserGroups.RemoveFromAllGroupsAsync(userId); + } + public async Task AddConnectionToGroup(string connectionId, string groupName) { if (string.IsNullOrEmpty(connectionId)) diff --git a/src/SignalRServiceExtension/Client/IAzureSignalRSender.cs b/src/SignalRServiceExtension/Client/IAzureSignalRSender.cs index 47534104..69003d6e 100644 --- a/src/SignalRServiceExtension/Client/IAzureSignalRSender.cs +++ b/src/SignalRServiceExtension/Client/IAzureSignalRSender.cs @@ -14,6 +14,7 @@ internal interface IAzureSignalRSender Task SendToGroup(string group, SignalRData data); Task AddUserToGroup(string userId, string groupName); Task RemoveUserFromGroup(string userId, string groupName); + Task RemoveUserFromAllGroups(string userId); Task AddConnectionToGroup(string connectionId, string groupName); Task RemoveConnectionFromGroup(string connectionId, string groupName); } diff --git a/test/SignalRServiceExtension.Tests/SignalRAsyncCollectorTests.cs b/test/SignalRServiceExtension.Tests/SignalRAsyncCollectorTests.cs index 255ec57b..9287ea5a 100644 --- a/test/SignalRServiceExtension.Tests/SignalRAsyncCollectorTests.cs +++ b/test/SignalRServiceExtension.Tests/SignalRAsyncCollectorTests.cs @@ -123,6 +123,26 @@ await collector.AddAsync(new SignalRGroupAction Assert.Equal("group1", actualData.Arguments[1]); } + [Fact] + public async Task AddAsync_WithUserId_CallsRemoveUserFromAllGroups() + { + var signalRSenderMock = new Mock(); + var collector = new SignalRAsyncCollector(signalRSenderMock.Object); + + await collector.AddAsync(new SignalRGroupAction + { + UserId = "userId1", + Action = GroupAction.RemoveAll + }); + + signalRSenderMock.Verify( + c => c.RemoveUserFromAllGroups("userId1"), + Times.Once); + signalRSenderMock.VerifyNoOtherCalls(); + var actualData = signalRSenderMock.Invocations[0]; + Assert.Equal("userId1", actualData.Arguments[0]); + } + [Fact] public async Task AddAsync_InvalidTypeThrowException() {