Add support for users (#22)
* Add UserId to SignalRConnectionInfoAttribute * Refactor, add moq - Rename IAzureSignalRClient to IAzureSignalRSender - Add SignalRData class - Change tests to use moq - Rename SendMessage to SendToAll * Add support for users * Make SignalRData internal * Change a few variable names * Update auth sample * Add direct messaging to simple chat * Add App Service Auth sample * Simplify function * Fix ready message on index.html * Update index.html to work with old and new SignalRConnectionInfo * Remove logging * Merge dev into antchu/add-users * Remove support for sending a message to more than one user * Bump to WebJobs SDK 3.0.0-rc1
This commit is contained in:
Родитель
f9774bb791
Коммит
441dc5f6bc
|
@ -7,6 +7,7 @@
|
|||
<MicroBuildCorePackageVersion>0.3.0</MicroBuildCorePackageVersion>
|
||||
<MicrosoftAzureWebJobsPackageVersion>3.0.0-rc1</MicrosoftAzureWebJobsPackageVersion>
|
||||
<MicrosoftNETTestSdkPackageVersion>15.8.0</MicrosoftNETTestSdkPackageVersion>
|
||||
<MoqPackageVersion>4.9.0</MoqPackageVersion>
|
||||
<SystemIdentityModelTokensJwtPackageVersion>5.1.4</SystemIdentityModelTokensJwtPackageVersion>
|
||||
<XunitPackageVersion>2.4.0</XunitPackageVersion>
|
||||
<XunitRunnerVisualstudioPackageVersion>2.4.0</XunitRunnerVisualstudioPackageVersion>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
(function () {
|
||||
let authToken = ""
|
||||
|
||||
if (window.location.hash) {
|
||||
const match = window.location.hash.match(/\btoken=([^&]+)/)
|
||||
if (match && match[1]) {
|
||||
authToken = JSON.parse(decodeURIComponent(match[1])).authenticationToken
|
||||
sessionStorage.setItem('authToken', authToken)
|
||||
history.pushState("", document.title, window.location.pathname + window.location.search)
|
||||
}
|
||||
}
|
||||
|
||||
if (!authToken) {
|
||||
authToken = sessionStorage.getItem('authToken')
|
||||
}
|
||||
|
||||
window.auth = {
|
||||
token: authToken,
|
||||
loginUrl: window.apiBaseUrl +
|
||||
'/.auth/login/twitter?session_mode=token&post_login_redirect_url=' +
|
||||
encodeURIComponent(window.location.href),
|
||||
logout: function() {
|
||||
sessionStorage.removeItem('authToken')
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}())
|
|
@ -0,0 +1,178 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>Serverless Chat</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css">
|
||||
<script>
|
||||
window.apiBaseUrl = 'https://test-auth-chat.azurewebsites.net';
|
||||
//window.apiBaseUrl = 'http://localhost:7071';
|
||||
</script>
|
||||
<script src="auth.js"></script>
|
||||
<style>
|
||||
.slide-fade-enter-active, .slide-fade-leave-active {
|
||||
transition: all 1s ease;
|
||||
}
|
||||
.slide-fade-enter, .slide-fade-leave-to {
|
||||
height: 0px;
|
||||
overflow-y: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p> </p>
|
||||
<div id="app" class="container">
|
||||
<h3>Serverless chat</h3>
|
||||
<div v-if="authenticated">
|
||||
You are logged in [<a href="#" v-on:click.prevent="logout">Logout</a>]
|
||||
</div>
|
||||
<div v-if="!authenticated">
|
||||
<a href="#" v-on:click.prevent="login">Login</a>
|
||||
</div>
|
||||
<div class="row" v-if="authenticated && ready">
|
||||
<div class="signalr-demo col-sm">
|
||||
<hr />
|
||||
<form v-on:submit.prevent="sendNewMessage">
|
||||
<input type="text" v-model="newMessage" id="message-box" class="form-control" placeholder="Type message here..." />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="authenticated && !ready">
|
||||
<div class="col-sm">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="ready">
|
||||
<transition-group name="slide-fade" tag="div">
|
||||
<div class="row" v-for="message in messages" v-bind:key="message.id">
|
||||
<div class="col-sm">
|
||||
<hr />
|
||||
<div>
|
||||
<img :src="message.avatarUrl" />
|
||||
<div style="display: inline-block; padding-left: 12px;">
|
||||
<div>
|
||||
<a href="#" v-on:click.prevent="sendPrivateMessage(message.sender)">
|
||||
<span class="text-info small"><strong>{{ message.sender }}</strong></span>
|
||||
</a>
|
||||
<span v-if="message.isPrivate" class="badge badge-secondary">private message</small>
|
||||
</div>
|
||||
<div>
|
||||
{{ message.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.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/timeago.js@3.0.2/dist/timeago.min.js"></script>
|
||||
|
||||
<script>
|
||||
const data = {
|
||||
authenticated: false,
|
||||
username: '',
|
||||
newMessage: '',
|
||||
messages: [],
|
||||
ready: false
|
||||
};
|
||||
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
data: data,
|
||||
methods: {
|
||||
sendNewMessage: function () {
|
||||
sendMessage(this.username, null, this.newMessage);
|
||||
this.newMessage = '';
|
||||
},
|
||||
login: function () {
|
||||
window.location.href = window.auth.loginUrl;
|
||||
},
|
||||
logout: window.auth.logout,
|
||||
sendPrivateMessage: function (recipient) {
|
||||
const messageText = prompt('Send private message to ' + recipient);
|
||||
if (messageText) {
|
||||
sendMessage(this.username, recipient, messageText);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const apiBaseUrl = window.apiBaseUrl;
|
||||
const hubName = 'authchat';
|
||||
|
||||
if (window.auth.token) {
|
||||
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: {}
|
||||
};
|
||||
if (window.auth.token) {
|
||||
config.headers['X-ZUMO-AUTH'] = window.auth.token;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function getConnectionInfo() {
|
||||
return axios.post(`${apiBaseUrl}/api/negotiate`, null, getAxiosConfig())
|
||||
.then(resp => resp.data);
|
||||
}
|
||||
|
||||
function sendMessage(sender, recipient, messageText) {
|
||||
return axios.post(`${apiBaseUrl}/api/messages`, {
|
||||
recipient: recipient,
|
||||
sender: sender,
|
||||
text: messageText
|
||||
}, getAxiosConfig()).then(resp => resp.data);
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
function newMessage(message) {
|
||||
if (message.sender) {
|
||||
message.avatarUrl = `https://avatars.io/twitter/${message.sender}/small`;
|
||||
} else {
|
||||
message.avatarUrl = 'https://avatars.io/static/default_48.jpg';
|
||||
}
|
||||
if (!message.sender) {
|
||||
message.sender = "anonymous";
|
||||
}
|
||||
message.id = counter++; // vue transitions need an id
|
||||
data.messages.unshift(message);
|
||||
}
|
||||
|
||||
data.authenticated = window.auth && window.auth.token;
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,264 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# Azure Functions localsettings file
|
||||
local.settings.json
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"ms-azuretools.vscode-azurefunctions",
|
||||
"ms-vscode.csharp"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to C# Functions",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processId": "${command:azureFunctions.pickProcess}"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"azureFunctions.projectRuntime": "beta",
|
||||
"azureFunctions.projectLanguage": "C#",
|
||||
"azureFunctions.templateFilter": "Verified",
|
||||
"azureFunctions.deploySubpath": "bin/Release/netstandard2.0/publish"
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "clean",
|
||||
"command": "dotnet clean",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet build",
|
||||
"type": "shell",
|
||||
"dependsOn": "clean",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "clean release",
|
||||
"command": "dotnet clean --configuration Release",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet publish --configuration Release",
|
||||
"type": "shell",
|
||||
"dependsOn": "clean release",
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Run Functions Host",
|
||||
"identifier": "runFunctionsHost",
|
||||
"type": "shell",
|
||||
"dependsOn": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/bin/Debug/netstandard2.0"
|
||||
},
|
||||
"command": "func host start",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.19" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\SignalRServiceExtension\Microsoft.Azure.WebJobs.Extensions.SignalRService.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="host.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.sample.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27703.2047
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionApp", "FunctionApp.csproj", "{185119A1-81E7-4A9C-BFD7-C3C976BDA463}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.SignalRService", "..\..\..\..\src\SignalRServiceExtension\Microsoft.Azure.WebJobs.Extensions.SignalRService.csproj", "{43AD6D39-E440-4812-A86F-22EA23E62456}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{185119A1-81E7-4A9C-BFD7-C3C976BDA463}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{185119A1-81E7-4A9C-BFD7-C3C976BDA463}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{185119A1-81E7-4A9C-BFD7-C3C976BDA463}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{185119A1-81E7-4A9C-BFD7-C3C976BDA463}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{43AD6D39-E440-4812-A86F-22EA23E62456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{43AD6D39-E440-4812-A86F-22EA23E62456}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{43AD6D39-E440-4812-A86F-22EA23E62456}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{43AD6D39-E440-4812-A86F-22EA23E62456}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {DBE75EA3-2A43-47B5-8806-859D6045A793}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace FunctionApp
|
||||
{
|
||||
public static class Functions
|
||||
{
|
||||
[FunctionName("negotiate")]
|
||||
public static SignalRConnectionInfo GetSignalRInfo(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req,
|
||||
[SignalRConnectionInfo(HubName = "authchat", UserId = "{headers.x-ms-client-principal-name}")]
|
||||
SignalRConnectionInfo connectionInfo)
|
||||
{
|
||||
return connectionInfo;
|
||||
}
|
||||
|
||||
[FunctionName("messages")]
|
||||
public static Task SendMessage(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequest req,
|
||||
[SignalR(HubName = "authchat")]IAsyncCollector<SignalRMessage> signalRMessages)
|
||||
{
|
||||
var message = DeserializeFromStream<ChatMessage>(req.Body);
|
||||
req.Headers.TryGetValue("x-ms-client-principal-name", out var sender);
|
||||
|
||||
if (!string.IsNullOrEmpty(sender))
|
||||
{
|
||||
message.sender = sender;
|
||||
}
|
||||
|
||||
string userId = null;
|
||||
message.isPrivate = !string.IsNullOrEmpty(message.recipient);
|
||||
if (message.isPrivate)
|
||||
{
|
||||
userId = message.recipient;
|
||||
}
|
||||
|
||||
return signalRMessages.AddAsync(
|
||||
new SignalRMessage
|
||||
{
|
||||
UserId = userId,
|
||||
Target = "newMessage",
|
||||
Arguments = new [] { message }
|
||||
});
|
||||
}
|
||||
|
||||
private static T DeserializeFromStream<T>(Stream stream)
|
||||
{
|
||||
var serializer = new JsonSerializer();
|
||||
|
||||
using (var sr = new StreamReader(stream))
|
||||
using (var jsonTextReader = new JsonTextReader(sr))
|
||||
{
|
||||
return serializer.Deserialize<T>(jsonTextReader);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChatMessage
|
||||
{
|
||||
public string sender { get; set; }
|
||||
public string text { get; set; }
|
||||
public string recipient { get; set; }
|
||||
public bool isPrivate { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"AzureWebJobsStorage": "",
|
||||
"AzureWebJobsDashboard": "",
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
|
||||
"AzureSignalRConnectionString": "<signalr-connection-string>"
|
||||
},
|
||||
"Host": {
|
||||
"LocalHttpPort": 7071,
|
||||
"CORS": "*"
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@
|
|||
(function () {
|
||||
let username;
|
||||
while (!username && username !== null) {
|
||||
username = prompt('Enter a username');
|
||||
username = prompt('Enter a username').replace(/\W/g, '_');
|
||||
}
|
||||
|
||||
if (username === null) return;
|
||||
|
@ -47,7 +47,17 @@
|
|||
|
||||
connection.on('newMessage', (message) => {
|
||||
const newMessage = document.createElement('li');
|
||||
newMessage.appendChild(document.createTextNode(`${message.sender}: ${message.text}`));
|
||||
const senderLink = document.createElement('a');
|
||||
senderLink.href="javascript:void(0)";
|
||||
senderLink.innerText = message.sender;
|
||||
senderLink.addEventListener('click', () => {
|
||||
const directMessageText = prompt(`Send a direct message to ${message.sender}...`);
|
||||
if (directMessageText) {
|
||||
sendMessage(username, message.sender, directMessageText);
|
||||
}
|
||||
});
|
||||
newMessage.appendChild(senderLink);
|
||||
newMessage.appendChild(document.createTextNode(`: ${message.text}`));
|
||||
messages.appendChild(newMessage);
|
||||
});
|
||||
connection.onclose(() => console.log('disconnected'));
|
||||
|
@ -60,7 +70,7 @@
|
|||
messageForm.addEventListener('submit', ev => {
|
||||
ev.preventDefault();
|
||||
const message = messageBox.value;
|
||||
sendMessage(username, message);
|
||||
sendMessage(username, null, message);
|
||||
messageBox.value = '';
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
{
|
||||
"label": "Run Functions Host",
|
||||
"options": {
|
||||
"env": {
|
||||
"languageWorkers:node:arguments": "--inspect=5858"
|
||||
}
|
||||
},
|
||||
"identifier": "runFunctionsHost",
|
||||
"type": "shell",
|
||||
|
|
|
@ -12,10 +12,20 @@ module.exports = function (context, req) {
|
|||
};
|
||||
|
||||
if (req.method === 'POST') {
|
||||
context.bindings.signalRMessages = [{
|
||||
const message = req.body;
|
||||
const recipient = req.query.recipient;
|
||||
|
||||
const signalRMessage = {
|
||||
"target": "newMessage",
|
||||
"arguments": [req.body]
|
||||
}];
|
||||
"arguments": [ message ]
|
||||
};
|
||||
|
||||
if (recipient) {
|
||||
message.text = "(private message) " + message.text;
|
||||
signalRMessage.userId = recipient;
|
||||
}
|
||||
|
||||
context.bindings.signalRMessages = [ signalRMessage ];
|
||||
}
|
||||
|
||||
context.done();
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
{
|
||||
"type": "signalRConnectionInfo",
|
||||
"name": "connectionInfo",
|
||||
"userId": "{query.name}",
|
||||
"hubName": "simplechat",
|
||||
"direction": "in"
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
||||
{
|
||||
internal interface IAzureSignalRClient
|
||||
internal class SignalRData
|
||||
{
|
||||
Task SendMessage(string hubName, SignalRMessage message);
|
||||
public string Target { get; set; }
|
||||
public object[] Arguments { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
||||
{
|
||||
public class SignalRMessage
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
public string Target { get; set; }
|
||||
public object[] Arguments { get; set; }
|
||||
}
|
||||
|
|
|
@ -9,18 +9,31 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
|||
{
|
||||
public class SignalRMessageAsyncCollector : IAsyncCollector<SignalRMessage>
|
||||
{
|
||||
private readonly IAzureSignalRClient client;
|
||||
private readonly IAzureSignalRSender client;
|
||||
private readonly string hubName;
|
||||
|
||||
internal SignalRMessageAsyncCollector(IAzureSignalRClient client, string hubName)
|
||||
internal SignalRMessageAsyncCollector(IAzureSignalRSender client, string hubName)
|
||||
{
|
||||
this.client = client;
|
||||
this.hubName = hubName;
|
||||
}
|
||||
|
||||
public Task AddAsync(SignalRMessage item, CancellationToken cancellationToken = default(CancellationToken))
|
||||
public async Task AddAsync(SignalRMessage item, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return client.SendMessage(hubName, item);
|
||||
var data = new SignalRData
|
||||
{
|
||||
Target = item.Target,
|
||||
Arguments = item.Arguments
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(item.UserId))
|
||||
{
|
||||
await client.SendToAll(hubName, data).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await client.SendToUser(hubName, item.UserId, data).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
// 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.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
@ -14,9 +16,10 @@ using Microsoft.IdentityModel.Tokens;
|
|||
using Newtonsoft.Json;
|
||||
|
||||
[assembly:InternalsVisibleTo("SignalRServiceExtension.Tests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
||||
{
|
||||
internal class AzureSignalRClient : IAzureSignalRClient
|
||||
internal class AzureSignalRClient : IAzureSignalRSender
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
|
@ -29,10 +32,11 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
|||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
internal SignalRConnectionInfo GetClientConnectionInfo(string hubName)
|
||||
internal SignalRConnectionInfo GetClientConnectionInfo(string hubName, IEnumerable<Claim> claims = null)
|
||||
{
|
||||
var hubUrl = $"{BaseEndpoint}:5001/client/?hub={hubName}";
|
||||
var token = GenerateJwtBearer(null, hubUrl, null, DateTime.UtcNow.AddMinutes(30), AccessKey);
|
||||
var identity = new ClaimsIdentity(claims);
|
||||
var token = GenerateJwtBearer(null, hubUrl, identity, DateTime.UtcNow.AddMinutes(30), AccessKey);
|
||||
return new SignalRConnectionInfo
|
||||
{
|
||||
Url = hubUrl,
|
||||
|
@ -40,10 +44,11 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
|||
};
|
||||
}
|
||||
|
||||
internal SignalRConnectionInfo GetServerConnectionInfo(string hubName)
|
||||
internal SignalRConnectionInfo GetServerConnectionInfo(string hubName, string additionalPath = "")
|
||||
{
|
||||
var hubUrl = $"{BaseEndpoint}:5002/api/v1-preview/hub/{hubName}";
|
||||
var token = GenerateJwtBearer(null, hubUrl, null, DateTime.UtcNow.AddMinutes(30), AccessKey);
|
||||
var audienceUrl = $"{hubUrl}{additionalPath}";
|
||||
var token = GenerateJwtBearer(null, audienceUrl, null, DateTime.UtcNow.AddMinutes(30), AccessKey);
|
||||
return new SignalRConnectionInfo
|
||||
{
|
||||
Url = hubUrl,
|
||||
|
@ -51,10 +56,23 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
|||
};
|
||||
}
|
||||
|
||||
public Task SendMessage(string hubName, SignalRMessage message)
|
||||
public Task SendToAll(string hubName, SignalRData data)
|
||||
{
|
||||
var connectionInfo = GetServerConnectionInfo(hubName);
|
||||
return PostJsonAsync(connectionInfo.Url, message, connectionInfo.AccessToken);
|
||||
return PostJsonAsync(connectionInfo.Url, data, connectionInfo.AccessToken);
|
||||
}
|
||||
|
||||
public Task SendToUser(string hubName, string userId, SignalRData data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(userId)} cannot be null or empty");
|
||||
}
|
||||
|
||||
var userIdsSegment = $"/user/{userId}";
|
||||
var connectionInfo = GetServerConnectionInfo(hubName, userIdsSegment);
|
||||
var uri = $"{connectionInfo.Url}{userIdsSegment}";
|
||||
return PostJsonAsync(uri, data, connectionInfo.AccessToken);
|
||||
}
|
||||
|
||||
private (string EndPoint, string AccessKey) ParseConnectionString(string connectionString)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
||||
{
|
||||
internal interface IAzureSignalRSender
|
||||
{
|
||||
Task SendToAll(string hubName, SignalRData data);
|
||||
Task SendToUser(string hubName, string userId, SignalRData data);
|
||||
}
|
||||
}
|
|
@ -2,8 +2,10 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Azure.WebJobs.Description;
|
||||
using Microsoft.Azure.WebJobs.Host.Config;
|
||||
using Microsoft.Azure.WebJobs.Logging;
|
||||
|
@ -96,7 +98,8 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
|||
private SignalRConnectionInfo GetClientConnectionInfo(SignalRConnectionInfoAttribute attribute)
|
||||
{
|
||||
var signalR = new AzureSignalRClient(attribute.ConnectionStringSetting, httpClient);
|
||||
return signalR.GetClientConnectionInfo(attribute.HubName);
|
||||
var claims = attribute.GetClaims();
|
||||
return signalR.GetClientConnectionInfo(attribute.HubName, claims);
|
||||
}
|
||||
|
||||
private string FirstOrDefault(params string[] values)
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Azure.WebJobs.Description;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
||||
|
@ -15,5 +17,18 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
|
|||
|
||||
[AutoResolve]
|
||||
public string HubName { get; set; }
|
||||
|
||||
[AutoResolve]
|
||||
public string UserId { get; set; }
|
||||
|
||||
internal IEnumerable<Claim> GetClaims()
|
||||
{
|
||||
var claims = new List<Claim>();
|
||||
if (!string.IsNullOrEmpty(UserId))
|
||||
{
|
||||
claims.Add(new Claim(ClaimTypes.NameIdentifier, UserId));
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -38,6 +39,27 @@ namespace SignalRServiceExtension.Tests
|
|||
Assert.Equal(expectedUrl, info.Url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AzureSignalRClient_GetClientConnectionInfoWithUserId_ReturnsValidInfoWithUserId()
|
||||
{
|
||||
var azureSignalR = new AzureSignalRClient("Endpoint=https://foo.service.signalr.net;AccessKey=/abcdefghijklmnopqrstu/v/wxyz11111111111111=;", null);
|
||||
var claims = new []
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, "foo")
|
||||
};
|
||||
|
||||
var info = azureSignalR.GetClientConnectionInfo("chat", claims);
|
||||
|
||||
const string expectedEndpoint = "https://foo.service.signalr.net:5001/client/?hub=chat";
|
||||
var claimsPrincipal = TestHelpers.EnsureValidAccessToken(
|
||||
audience: expectedEndpoint,
|
||||
signingKey: "/abcdefghijklmnopqrstu/v/wxyz11111111111111=",
|
||||
accessToken: info.AccessToken);
|
||||
Assert.Contains(claimsPrincipal.Claims,
|
||||
c => c.Type == ClaimTypes.NameIdentifier && c.Value == "foo");
|
||||
Assert.Equal(expectedEndpoint, info.Url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AzureSignalRClient_GetServerConnectionInfo_ReturnsValidInfo()
|
||||
{
|
||||
|
@ -54,7 +76,7 @@ namespace SignalRServiceExtension.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendMessage_CallsAzureSignalRService()
|
||||
public async Task SendToAll_CallsAzureSignalRService()
|
||||
{
|
||||
var connectionString = "Endpoint=https://foo.service.signalr.net;AccessKey=/abcdefghijklmnopqrstu/v/wxyz11111111111111=;";
|
||||
var hubName = "chat";
|
||||
|
@ -62,7 +84,7 @@ namespace SignalRServiceExtension.Tests
|
|||
var httpClient = new HttpClient(requestHandler);
|
||||
var azureSignalR = new AzureSignalRClient(connectionString, httpClient);
|
||||
|
||||
await azureSignalR.SendMessage(hubName, new SignalRMessage
|
||||
await azureSignalR.SendToAll(hubName, new SignalRData
|
||||
{
|
||||
Target = "newMessage",
|
||||
Arguments = new object[] { "arg1", "arg2" }
|
||||
|
@ -86,6 +108,43 @@ namespace SignalRServiceExtension.Tests
|
|||
accessToken: authorizationHeader.Parameter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendToUser_CallsAzureSignalRService()
|
||||
{
|
||||
var connectionString = "Endpoint=https://foo.service.signalr.net;AccessKey=/abcdefghijklmnopqrstu/v/wxyz11111111111111=;";
|
||||
var hubName = "chat";
|
||||
var requestHandler = new FakeHttpMessageHandler();
|
||||
var httpClient = new HttpClient(requestHandler);
|
||||
var azureSignalR = new AzureSignalRClient(connectionString, httpClient);
|
||||
|
||||
await azureSignalR.SendToUser(
|
||||
hubName,
|
||||
"userId1",
|
||||
new SignalRData
|
||||
{
|
||||
Target = "newMessage",
|
||||
Arguments = new object[] { "arg1", "arg2" }
|
||||
});
|
||||
|
||||
var baseEndpoint = "https://foo.service.signalr.net:5002/api/v1-preview/hub/chat";
|
||||
var expectedEndpoint = $"{baseEndpoint}/user/userId1";
|
||||
var request = requestHandler.HttpRequestMessage;
|
||||
Assert.Equal("application/json", request.Content.Headers.ContentType.MediaType);
|
||||
Assert.Equal(expectedEndpoint, request.RequestUri.AbsoluteUri);
|
||||
|
||||
var actualRequestBody = JsonConvert.DeserializeObject<SignalRMessage>(await request.Content.ReadAsStringAsync());
|
||||
Assert.Equal("newMessage", actualRequestBody.Target);
|
||||
Assert.Equal("arg1", actualRequestBody.Arguments[0]);
|
||||
Assert.Equal("arg2", actualRequestBody.Arguments[1]);
|
||||
|
||||
var authorizationHeader = request.Headers.Authorization;
|
||||
Assert.Equal("Bearer", authorizationHeader.Scheme);
|
||||
TestHelpers.EnsureValidAccessToken(
|
||||
audience: expectedEndpoint,
|
||||
signingKey: "/abcdefghijklmnopqrstu/v/wxyz11111111111111=",
|
||||
accessToken: authorizationHeader.Parameter);
|
||||
}
|
||||
|
||||
private class FakeHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
public HttpRequestMessage HttpRequestMessage { get; private set; }
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
|
||||
using Xunit;
|
||||
|
||||
namespace SignalRServiceExtension.Tests
|
||||
{
|
||||
public class SignalRConnectionInfoAttributeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetClaims_WithNoUserId_ReturnsEmptyClaims()
|
||||
{
|
||||
var attr = new SignalRConnectionInfoAttribute
|
||||
{
|
||||
UserId = null
|
||||
};
|
||||
|
||||
var claims = attr.GetClaims();
|
||||
|
||||
Assert.Empty(claims);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClaims_WithUserId_ReturnsUserIdInClaims()
|
||||
{
|
||||
var attr = new SignalRConnectionInfoAttribute
|
||||
{
|
||||
UserId = "foo"
|
||||
};
|
||||
|
||||
var claims = attr.GetClaims();
|
||||
|
||||
Assert.Contains(claims, c => c.Type == ClaimTypes.NameIdentifier && c.Value == "foo");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace SignalRServiceExtension.Tests
|
||||
|
@ -10,33 +12,46 @@ namespace SignalRServiceExtension.Tests
|
|||
public class SignalRMessageAsyncCollectorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AddAsync_CallsAzureSignalRClient()
|
||||
public async Task AddAsync_WithBroadcastMessage_CallsSendToAll()
|
||||
{
|
||||
var client = new FakeAzureSignalRClient();
|
||||
var collector = new SignalRMessageAsyncCollector(client, "foo");
|
||||
var signalRSenderMock = new Mock<IAzureSignalRSender>();
|
||||
var collector = new SignalRMessageAsyncCollector(signalRSenderMock.Object, "chathub");
|
||||
|
||||
await collector.AddAsync(new SignalRMessage
|
||||
{
|
||||
Target = "newMessage",
|
||||
Arguments = new object[] { "arg1", "arg2" }
|
||||
});
|
||||
|
||||
var actualSendMessageParams = client.SendMessageParams;
|
||||
Assert.Equal("foo", actualSendMessageParams.hubName);
|
||||
Assert.Equal("newMessage", actualSendMessageParams.message.Target);
|
||||
Assert.Equal("arg1", actualSendMessageParams.message.Arguments[0]);
|
||||
Assert.Equal("arg2", actualSendMessageParams.message.Arguments[1]);
|
||||
|
||||
signalRSenderMock.Verify(c => c.SendToAll("chathub", It.IsAny<SignalRData>()), Times.Once);
|
||||
signalRSenderMock.VerifyNoOtherCalls();
|
||||
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]);
|
||||
}
|
||||
|
||||
private class FakeAzureSignalRClient : IAzureSignalRClient
|
||||
[Fact]
|
||||
public async Task AddAsync_WithUserId_CallsSendToUser()
|
||||
{
|
||||
public (string hubName, SignalRMessage message) SendMessageParams { get; private set; }
|
||||
|
||||
public Task SendMessage(string hubName, SignalRMessage message)
|
||||
var signalRSenderMock = new Mock<IAzureSignalRSender>();
|
||||
var collector = new SignalRMessageAsyncCollector(signalRSenderMock.Object, "chathub");
|
||||
|
||||
await collector.AddAsync(new SignalRMessage
|
||||
{
|
||||
SendMessageParams = (hubName, message);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
UserId = "userId1",
|
||||
Target = "newMessage",
|
||||
Arguments = new object[] { "arg1", "arg2" }
|
||||
});
|
||||
|
||||
signalRSenderMock.Verify(
|
||||
c => c.SendToUser("chathub", "userId1", 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]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
||||
<PackageReference Include="Moq" Version="$(MoqPackageVersion)" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="$(SystemIdentityModelTokensJwtPackageVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualstudioPackageVersion)" />
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
|
@ -10,7 +11,7 @@ namespace SignalRServiceExtension.Tests.Utils
|
|||
{
|
||||
class TestHelpers
|
||||
{
|
||||
internal static void EnsureValidAccessToken(string audience, string signingKey, string accessToken)
|
||||
internal static ClaimsPrincipal EnsureValidAccessToken(string audience, string signingKey, string accessToken)
|
||||
{
|
||||
var validationParameters =
|
||||
new TokenValidationParameters
|
||||
|
@ -26,7 +27,7 @@ namespace SignalRServiceExtension.Tests.Utils
|
|||
expires.HasValue && expires > DateTime.UtcNow.AddMinutes(5) // at least 5 minutes
|
||||
};
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
handler.ValidateToken(accessToken, validationParameters, out _);
|
||||
return handler.ValidateToken(accessToken, validationParameters, out _);
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче