Add connection id related apis (#68)

* Add connection id related apis

* Update to resolve parameter order

* Add sample with connectionId APIs

* Encode the connectonId
This commit is contained in:
Chenyang Liu 2019-07-22 15:18:54 +08:00 коммит произвёл GitHub
Родитель bcc4e549f8
Коммит 415e35d9cb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 410 добавлений и 132 удалений

Просмотреть файл

@ -26,12 +26,18 @@ public class SignalRGroupAction {
* @param groupName Group to add user to or remove user from
* @param userId User to add to or remove from group
*/
public SignalRGroupAction(String action, String groupName, String userId) {
public SignalRGroupAction(String action, String groupName, String userId, String connectionId) {
this.action = action;
this.groupName = groupName;
this.userId = userId;
this.connectionId = connectionId;
}
/**
* Connection to add to or remove from group
*/
public String connectionId = "";
/**
* User to add to or remove from group
*/

Просмотреть файл

@ -35,6 +35,11 @@ public class SignalRMessage {
this.arguments.addAll(Arrays.asList(arguments));
}
/**
* ConnectionId to send the message to
*/
public String connectionId = "";
/**
* User to send the message to
*/

Просмотреть файл

@ -5,7 +5,7 @@
</PropertyGroup>
<PropertyGroup Label="Package Versions">
<MicroBuildCorePackageVersion>0.3.0</MicroBuildCorePackageVersion>
<MicrosoftAzureSignalRManagement>1.0.0-preview1-10420</MicrosoftAzureSignalRManagement>
<MicrosoftAzureSignalRManagement>1.0.0-preview1-10449</MicrosoftAzureSignalRManagement>
<MicrosoftAzureWebJobsPackageVersion>3.0.4</MicrosoftAzureWebJobsPackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.8.0</MicrosoftNETTestSdkPackageVersion>
<MoqPackageVersion>4.9.0</MoqPackageVersion>

Просмотреть файл

@ -6,6 +6,7 @@
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
$(RestoreSources);
https://api.nuget.org/v3/index.json;
https://www.myget.org/F/azure-signalr-dev/api/v3/index.json
</RestoreSources>
<RestoreSources Condition="'$(UseLocalFeed)' == 'true'">
$(LocalFeed);

Просмотреть файл

@ -1,4 +1,5 @@
<html>
<head>
<title>Serverless Chat</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css">
@ -6,10 +7,13 @@
window.apiBaseUrl = 'http://localhost:7071';
</script>
<style>
.slide-fade-enter-active, .slide-fade-leave-active {
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 1s ease;
}
.slide-fade-enter, .slide-fade-leave-to {
.slide-fade-enter,
.slide-fade-leave-to {
height: 0px;
overflow-y: hidden;
opacity: 0;
@ -29,7 +33,8 @@
<label for="checkbox">Send To Default Group: {{ this.defaultgroup }}</label>
</div>
<form v-on:submit.prevent="sendNewMessage(checked)">
<input type="text" v-model="newMessage" id="message-box" class="form-control" placeholder="Type message here..." />
<input type="text" v-model="newMessage" id="message-box" class="form-control"
placeholder="Type message here..." />
</form>
</div>
</div>
@ -49,13 +54,21 @@
<a href="#" v-on:click.prevent="sendPrivateMessage(message.Sender)">
<span class="text-info small"><strong>{{ message.Sender || message.sender }}</strong></span>
</a>
<a href="#" v-on:click.prevent="addToGroup(message.Sender || message.sender)">
<span v-if="message.ConnectionId || message.connectionId" class="badge badge-secondary">Connection: {{ message.ConnectionId || message.connectionId }}</span>
<a href="#" v-on:click.prevent="addToGroup(null, message.Sender || message.sender)">
<span class="badge badge-primary">AddGroup</span>
</a>
<a href="#" v-on:click.prevent="removeFromGroup(message.Sender || message.sender)">
<span class="badge badge-primary">RemoveGroup</span>
</a>
<span v-if="message.IsPrivate || message.isPrivate" class="badge badge-secondary">private message</span>
<a href="#" v-on:click.prevent="removeFromGroup(null, message.Sender || message.sender)">
<span class="badge badge-primary">RemoveGroup</span>
</a>
<a href="#" v-on:click.prevent="addToGroup(message.ConnectionId || message.connectionId, message.Sender || message.sender)">
<span v-if="message.ConnectionId || message.connectionId" class="badge badge-primary">AddConnectionToGroup</span>
</a>
<a href="#" v-on:click.prevent="removeFromGroup(message.ConnectionId || message.connectionId, message.Sender || message.sender)">
<span v-if="message.ConnectionId || message.connectionId" class="badge badge-primary">RemoveConnectionFromGroup</span>
</a>
<span v-if="message.IsPrivate || message.isPrivate" class="badge badge-secondary">private
message</span>
</div>
<div>
{{ message.Text || message.text }}
@ -64,123 +77,146 @@
</div>
</div>
</div>
</transition-group>
</div>
</transition-group>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.0.3/dist/browser/signalr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.0.3/dist/browser/signalr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
<script>
const data = {
username: '',
defaultgroup: 'AzureSignalR',
checked: false,
newMessage: '',
messages: [],
ready: false
};
const app = new Vue({
el: '#app',
data: data,
methods: {
sendNewMessage: function (isToGroup) {
if(isToGroup) {
sendMessage(this.username, null, this.defaultgroup, this.newMessage);
}
else {
sendMessage(this.username, null, null, this.newMessage);
}
this.newMessage = '';
},
sendPrivateMessage: function (recipient) {
const messageText = prompt('Send private message to ' + recipient);
if (messageText) {
sendMessage(this.username, recipient, null, messageText);
}
},
addToGroup: function (recipient) {
var r = confirm('Add user ' + recipient + ' to group: ' + this.defaultgroup);
if(r) {
addGroup(this.username, recipient, this.defaultgroup);
}
},
removeFromGroup: function (recipient) {
var r = confirm('Remove user ' + recipient + ' from group: ' + this.defaultgroup);
if(r) {
removeGroup(this.username, recipient, this.defaultgroup);
<script>
const data = {
username: '',
defaultgroup: 'AzureSignalR',
checked: false,
newMessage: '',
messages: [],
myConnectionId: '',
ready: false
};
const app = new Vue({
el: '#app',
data: data,
methods: {
sendNewMessage: function (isToGroup) {
if (isToGroup) {
sendMessage(this.username, null, this.defaultgroup, this.newMessage);
}
else {
sendMessage(this.username, null, null, this.newMessage);
}
this.newMessage = '';
},
sendPrivateMessage: function (recipient) {
const messageText = prompt('Send private message to ' + recipient);
if (messageText) {
sendMessage(this.username, recipient, null, messageText);
}
},
addToGroup: function (connectionId, recipient) {
var r;
if (connectionId) {
r = confirm('Add connection ' + connectionId + ' to group: ' + this.defaultgroup);
} else {
r = confirm('Add user ' + recipient + ' to group: ' + this.defaultgroup);
}
if (r) {
addGroup(this.username, recipient, connectionId, this.defaultgroup);
}
},
removeFromGroup: function (connectionId, recipient) {
var r;
if (connectionId) {
r = confirm('Remove connection ' + connectionId + ' from group: ' + this.defaultgroup);
} else {
r = confirm('Remove user ' + recipient + ' from group: ' + this.defaultgroup);
}
if (r) {
removeGroup(this.username, recipient, connectionId, this.defaultgroup);
}
}
}
});
const apiBaseUrl = prompt('Enter the Azure Function app base URL', window.apiBaseUrl);
data.username = prompt("Enter your username");
if (!data.username) {
alert("No username entered. Reload page and try again.");
throw "No username entered";
}
});
const apiBaseUrl = prompt('Enter the Azure Function app base URL', window.apiBaseUrl);
data.username = prompt("Enter your username");
if (!data.username) {
alert("No username entered. Reload page and try again.");
throw "No username entered";
}
getConnectionInfo().then(info => {
// make compatible with old and new SignalRConnectionInfo
info.accessToken = info.accessToken || info.accessKey;
info.url = info.url || info.endpoint;
data.ready = true;
const options = {
accessTokenFactory: () => info.accessToken
};
const connection = new signalR.HubConnectionBuilder()
.withUrl(info.url, options)
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on('newMessage', newMessage);
connection.onclose(() => console.log('disconnected'));
console.log('connecting...');
connection.start()
.then(() => console.log('connected!'))
.catch(console.error);
}).catch(alert);
function getAxiosConfig() {
const config = {
headers: {'x-ms-signalr-userid': data.username}
};
return config;
}
function getConnectionInfo() {
return axios.post(`${apiBaseUrl}/api/negotiate`, null , getAxiosConfig())
.then(resp => resp.data);
}
function sendMessage(sender, recipient, groupname, messageText) {
return axios.post(`${apiBaseUrl}/api/messages`, {
recipient: recipient,
isPrivate: recipient != null,
groupname: groupname,
sender: sender,
text: messageText
}, getAxiosConfig()).then(resp => resp.data);
}
function addGroup(sender, recipient, groupName) {
return axios.post(`${apiBaseUrl}/api/addToGroup`, {
recipient: recipient,
groupname: groupName
}, getAxiosConfig()).then(resp => {
if(resp.status == 200) {
getConnectionInfo().then(info => {
// make compatible with old and new SignalRConnectionInfo
info.accessToken = info.accessToken || info.accessKey;
info.url = info.url || info.endpoint;
data.ready = true;
const options = {
accessTokenFactory: () => info.accessToken
};
const connection = new signalR.HubConnectionBuilder()
.withUrl(info.url, options)
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on('newMessage', newMessage);
connection.on('newConnection', newConnection)
connection.onclose(() => console.log('disconnected'));
console.log('connecting...');
connection.start()
.then(() => console.log('connected!'))
.catch(console.error);
}).catch(alert);
function getAxiosConfig() {
const config = {
headers: { 'x-ms-signalr-userid': data.username }
};
return config;
}
function getConnectionInfo() {
return axios.post(`${apiBaseUrl}/api/negotiate`, null, getAxiosConfig())
.then(resp => resp.data);
}
function sendMessage(sender, recipient, groupname, messageText) {
return axios.post(`${apiBaseUrl}/api/messages`, {
connectionId: data.myConnectionId,
recipient: recipient,
isPrivate: recipient != null,
groupname: groupname,
sender: sender,
text: messageText
}, getAxiosConfig()).then(resp => resp.data);
}
function addGroup(sender, recipient, connectionId, groupName) {
return axios.post(`${apiBaseUrl}/api/addToGroup`, {
connectionId: connectionId,
recipient: recipient,
groupname: groupName
}, getAxiosConfig()).then(resp => {
if (resp.status == 200) {
confirm("Add Successfully")
}});
}
function removeGroup(sender, recipient, groupName) {
return axios.post(`${apiBaseUrl}/api/removeFromGroup`, {
recipient: recipient,
groupname: groupName
}, getAxiosConfig()).then(resp => {
if(resp.status == 200) {
}
});
}
function removeGroup(sender, recipient, connectionId, groupName) {
return axios.post(`${apiBaseUrl}/api/removeFromGroup`, {
connectionId: connectionId,
recipient: recipient,
groupname: groupName
}, getAxiosConfig()).then(resp => {
if (resp.status == 200) {
confirm("Remove Successfully")
}});
}
let counter = 0;
function newMessage(message) {
message.id = counter++; // vue transitions need an id
data.messages.unshift(message);
}
</script>
}
});
}
let counter = 0;
function newMessage(message) {
message.id = counter++; // vue transitions need an id
data.messages.unshift(message);
};
function newConnection(message) {
data.myConnectionId = message.ConnectionId;
}
</script>
</body>
</html>

Просмотреть файл

@ -4,6 +4,7 @@
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.EventGrid" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.23" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -3,14 +3,17 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Azure.EventGrid.Models;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace FunctionApp
{
@ -39,7 +42,6 @@ namespace FunctionApp
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequest req,
[SignalR(HubName = "simplechat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body)));
return signalRMessages.AddAsync(
@ -60,10 +62,12 @@ namespace FunctionApp
var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body)));
var decodedfConnectionId = GetBase64DecodedString(message.ConnectionId);
return signalRGroupActions.AddAsync(
new SignalRGroupAction
{
ConnectionId = decodedfConnectionId,
UserId = message.Recipient,
GroupName = message.Groupname,
Action = GroupAction.Add
@ -78,23 +82,83 @@ namespace FunctionApp
var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body)));
var decodedfConnectionId = GetBase64DecodedString(message.ConnectionId);
return signalRGroupActions.AddAsync(
new SignalRGroupAction
{
ConnectionId = message.ConnectionId,
UserId = message.Recipient,
GroupName = message.Groupname,
Action = GroupAction.Remove
});
}
private static string GetBase64EncodedString(string source)
{
if (string.IsNullOrEmpty(source))
{
return source;
}
return Convert.ToBase64String(Encoding.UTF8.GetBytes(source));
}
private static string GetBase64DecodedString(string source)
{
if (string.IsNullOrEmpty(source))
{
return source;
}
return Encoding.UTF8.GetString(Convert.FromBase64String(source));
}
public static class EventGridTriggerCSharp
{
[FunctionName("onConnection")]
public static Task EventGridTest([EventGridTrigger]EventGridEvent eventGridEvent,
[SignalR(HubName = "simplechat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
if (eventGridEvent.EventType == "Microsoft.SignalRService.ClientConnectionConnected")
{
var message = ((JObject) eventGridEvent.Data).ToObject<SignalREvent>();
return signalRMessages.AddAsync(
new SignalRMessage
{
ConnectionId = message.ConnectionId,
Target = "newConnection",
Arguments = new[] { new ChatMessage
{
// ConnectionId is not recommand to send to client directly.
// Here's a simple encryption for an easier sample.
ConnectionId = GetBase64EncodedString(message.ConnectionId),
}}
});
}
return Task.CompletedTask;
}
}
public class ChatMessage
{
public string Sender { get; set; }
public string Text { get; set; }
public string Groupname { get; set; }
public string Recipient { get; set; }
public string ConnectionId { get; set; }
public bool IsPrivate { get; set; }
}
public class SignalREvent
{
public DateTime Timestamp { get; set; }
public string HubName { get; set; }
public string ConnectionId { get; set; }
public string UserId { get; set; }
}
}
}

Просмотреть файл

@ -38,12 +38,11 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
Arguments = message.Arguments
};
if (!string.IsNullOrEmpty(message.UserId) && !string.IsNullOrEmpty(message.GroupName))
if (!string.IsNullOrEmpty(message.ConnectionId))
{
throw new ArgumentException("GroupName and UserId can not be specified at the same time.");
await client.SendToConnection(hubName, message.ConnectionId, data).ConfigureAwait(false);
}
if (!string.IsNullOrEmpty(message.UserId))
else if (!string.IsNullOrEmpty(message.UserId))
{
await client.SendToUser(hubName, message.UserId, data).ConfigureAwait(false);
}
@ -59,13 +58,32 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
else if (convertItem.GetType() == typeof(SignalRGroupAction))
{
SignalRGroupAction groupAction = convertItem as SignalRGroupAction;
if (groupAction.Action == GroupAction.Add)
if (!string.IsNullOrEmpty(groupAction.ConnectionId))
{
await client.AddUserToGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false);
if (groupAction.Action == GroupAction.Add)
{
await client.AddConnectionToGroup(hubName, groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false);
}
else
{
await client.RemoveConnectionFromGroup(hubName, 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);
}
else
{
await client.RemoveUserFromGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false);
}
}
else
{
await client.RemoveUserFromGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false);
throw new ArgumentException($"ConnectionId and UserId cannot be null or empty together");
}
}
else

Просмотреть файл

@ -7,10 +7,19 @@ using System.Runtime.Serialization;
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
{
/// <summary>
/// Class that contains parameters needed for group operations.
/// Either the group operation on connectionId or userId is supported.
/// If connectionId and userId are both set, it will be resolved by the following order:
/// 1. ConnectionId
/// 2. UserId
/// </summary>
[JsonObject]
public class SignalRGroupAction
{
[JsonProperty("userId"), JsonRequired]
[JsonProperty("connectionId")]
public string ConnectionId { get; set; }
[JsonProperty("userId")]
public string UserId { get; set; }
[JsonProperty("groupName"), JsonRequired]
public string GroupName { get; set; }

Просмотреть файл

@ -5,9 +5,19 @@ using Newtonsoft.Json;
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
{
/// <summary>
/// Class that contains parameters needed for sending messages.
/// There are three kinds of scope to send, and if more than one
/// scopes are set, it will be resolved by the following order:
/// 1. ConnectionId
/// 2. UserId
/// 3. GroupName
/// </summary>
[JsonObject]
public class SignalRMessage
{
[JsonProperty("connectionId")]
public string ConnectionId { get; set; }
[JsonProperty("userId")]
public string UserId { get; set; }
[JsonProperty("groupName")]

Просмотреть файл

@ -60,6 +60,12 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
await serviceHubContext.Clients.All.SendCoreAsync(data.Target, data.Arguments);
}
public async Task SendToConnection(string hubName, string connectionId, SignalRData data)
{
var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName);
await serviceHubContext.Clients.Client(connectionId).SendCoreAsync(data.Target, data.Arguments);
}
public async Task SendToUser(string hubName, string userId, SignalRData data)
{
if (string.IsNullOrEmpty(userId))
@ -108,6 +114,34 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
await serviceHubContext.UserGroups.RemoveFromGroupAsync(userId, groupName);
}
public async Task AddConnectionToGroup(string hubName, string connectionId, string groupName)
{
if (string.IsNullOrEmpty(connectionId))
{
throw new ArgumentException($"{nameof(connectionId)} cannot be null or empty");
}
if (string.IsNullOrEmpty(groupName))
{
throw new ArgumentException($"{nameof(groupName)} cannot be null or empty");
}
var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName);
await serviceHubContext.Groups.AddToGroupAsync(connectionId, groupName);
}
public async Task RemoveConnectionFromGroup(string hubName, string connectionId, string groupName)
{
if (string.IsNullOrEmpty(connectionId))
{
throw new ArgumentException($"{nameof(connectionId)} cannot be null or empty");
}
if (string.IsNullOrEmpty(groupName))
{
throw new ArgumentException($"{nameof(groupName)} cannot be null or empty");
}
var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName);
await serviceHubContext.Groups.RemoveFromGroupAsync(connectionId, groupName);
}
private static IEnumerable<Claim> BuildJwtClaims(IEnumerable<Claim> customerClaims, string prefix)
{
if (customerClaims != null)

Просмотреть файл

@ -9,9 +9,12 @@ 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);
}
}

Просмотреть файл

@ -135,17 +135,108 @@ namespace SignalRServiceExtension.Tests
}
[Fact]
public async Task AddAsync_SendMessage_WithBothUserIdAndGroupNameThrowException()
public async Task AddAsync_SendMessage_WithBothUserIdAndGroupName_UsePriorityOrder()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRMessage>(signalRSenderMock.Object, "chathub");
var item = new SignalRMessage
await collector.AddAsync(new SignalRMessage
{
UserId = "user1",
GroupName = "group1",
Target = "newMessage",
Arguments = new object[] { "arg1", "arg2" }
});
signalRSenderMock.Verify(
c => c.SendToUser("chathub", "user1", It.IsAny<SignalRData>()),
Times.Once);
signalRSenderMock.VerifyNoOtherCalls();
var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[2];
Assert.Equal("newMessage", actualData.Target);
Assert.Equal("arg1", actualData.Arguments[0]);
Assert.Equal("arg2", actualData.Arguments[1]);
}
[Fact]
public async Task AddAsync_WithConnectionId_CallsSendToUser()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRMessage>(signalRSenderMock.Object, "chathub");
await collector.AddAsync(new SignalRMessage
{
ConnectionId = "connection1",
Target = "newMessage",
Arguments = new object[] { "arg1", "arg2" }
});
signalRSenderMock.Verify(
c => c.SendToConnection("chathub", "connection1", It.IsAny<SignalRData>()),
Times.Once);
signalRSenderMock.VerifyNoOtherCalls();
var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[2];
Assert.Equal("newMessage", actualData.Target);
Assert.Equal("arg1", actualData.Arguments[0]);
Assert.Equal("arg2", actualData.Arguments[1]);
}
[Fact]
public async Task AddAsync_WithConnectionId_CallsAddConnectionToGroup()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRGroupAction>(signalRSenderMock.Object, "chathub");
await collector.AddAsync(new SignalRGroupAction
{
ConnectionId = "connection1",
GroupName = "group1",
Action = GroupAction.Add
});
signalRSenderMock.Verify(
c => c.AddConnectionToGroup("chathub", "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]);
}
[Fact]
public async Task AddAsync_WithConnectionId_CallsRemoveConnectionFromGroup()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRGroupAction>(signalRSenderMock.Object, "chathub");
await collector.AddAsync(new SignalRGroupAction
{
ConnectionId = "connection1",
GroupName = "group1",
Action = GroupAction.Remove
});
signalRSenderMock.Verify(
c => c.RemoveConnectionFromGroup("chathub", "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]);
}
[Fact]
public async Task AddAsync_GroupOperation_WithoutParametersThrowException()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRGroupAction>(signalRSenderMock.Object, "chathub");
var item = new SignalRGroupAction
{
GroupName = "group1",
Action = GroupAction.Add
};
await Assert.ThrowsAsync<ArgumentException>(() => collector.AddAsync(item));