Scaffold some AAT stuff

This commit is contained in:
2022-06-19 17:35:33 -05:00
parent 3e938a8576
commit 770344422d
16 changed files with 275 additions and 47 deletions

View File

@@ -5,12 +5,8 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Controllers namespace Gameboard.ShogiUI.Sockets.Controllers
{ {
@@ -19,7 +15,6 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
[Authorize] [Authorize]
public class SocketController : ControllerBase public class SocketController : ControllerBase
{ {
private readonly ILogger<SocketController> logger;
private readonly ISocketTokenCache tokenCache; private readonly ISocketTokenCache tokenCache;
private readonly IGameboardManager gameboardManager; private readonly IGameboardManager gameboardManager;
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardRepository gameboardRepository;
@@ -33,7 +28,6 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
IGameboardRepository gameboardRepository, IGameboardRepository gameboardRepository,
ISocketConnectionManager connectionManager) ISocketConnectionManager connectionManager)
{ {
this.logger = logger;
this.tokenCache = tokenCache; this.tokenCache = tokenCache;
this.gameboardManager = gameboardManager; this.gameboardManager = gameboardManager;
this.gameboardRepository = gameboardRepository; this.gameboardRepository = gameboardRepository;

View File

@@ -8,9 +8,12 @@
<GenerateDocumentationFile>False</GenerateDocumentationFile> <GenerateDocumentationFile>False</GenerateDocumentationFile>
<SignAssembly>False</SignAssembly> <SignAssembly>False</SignAssembly>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>973a1f5f-ef25-4f1c-a24d-b0fc7d016ab8</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.0.0" />
<PackageReference Include="Azure.Identity" Version="1.6.0" />
<PackageReference Include="FluentValidation" Version="10.3.6" /> <PackageReference Include="FluentValidation" Version="10.3.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.5" />

View File

@@ -35,7 +35,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
{ {
await Task.Delay(TimeSpan.FromMinutes(1)); await Task.Delay(TimeSpan.FromMinutes(1));
Tokens.Remove(userName, out _); Tokens.Remove(userName, out _);
}); }).ConfigureAwait(false);
return guid; return guid;
} }

View File

@@ -60,16 +60,22 @@ namespace Gameboard.ShogiUI.Sockets
{ {
// TODO: Figure out how to make a middleware for sockets? // TODO: Figure out how to make a middleware for sockets?
var socketService = app.Services.GetRequiredService<ISocketService>(); var socketService = app.Services.GetRequiredService<ISocketService>();
var allowedOrigins = app.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>();
var origins = new[] {
"http://localhost:3000", "https://localhost:3000",
"http://127.0.0.1:3000", "https://127.0.0.1:3000",
"https://api.lucaserver.space", "https://lucaserver.space"
};
var socketOptions = new WebSocketOptions(); var socketOptions = new WebSocketOptions();
foreach (var o in origins) foreach (var origin in allowedOrigins)
socketOptions.AllowedOrigins.Add(o); socketOptions.AllowedOrigins.Add(origin);
app.UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials());
app.UseCors(options =>
{
options
.WithOrigins(allowedOrigins)
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Set-Cookie")
.AllowCredentials();
});
app.UseWebSockets(socketOptions); app.UseWebSockets(socketOptions);
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {

View File

@@ -0,0 +1,76 @@
{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"resourceGroupName": {
"type": "string",
"defaultValue": "DefaultResourceGroup-CUS",
"metadata": {
"_parameterType": "resourceGroup",
"description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
}
},
"resourceGroupLocation": {
"type": "string",
"defaultValue": "centralus",
"metadata": {
"_parameterType": "location",
"description": "Location of the resource group. Resource groups could have different location than resources."
}
},
"resourceLocation": {
"type": "string",
"defaultValue": "[parameters('resourceGroupLocation')]",
"metadata": {
"_parameterType": "location",
"description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
}
}
},
"resources": [
{
"type": "Microsoft.Resources/resourceGroups",
"name": "[parameters('resourceGroupName')]",
"location": "[parameters('resourceGroupLocation')]",
"apiVersion": "2019-10-01"
},
{
"type": "Microsoft.Resources/deployments",
"name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('GameboardShogiUISocketsv', subscription().subscriptionId)))]",
"resourceGroup": "[parameters('resourceGroupName')]",
"apiVersion": "2019-10-01",
"dependsOn": [
"[parameters('resourceGroupName')]"
],
"properties": {
"mode": "Incremental",
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"resources": [
{
"name": "GameboardShogiUISocketsv",
"type": "Microsoft.KeyVault/vaults",
"location": "[parameters('resourceLocation')]",
"properties": {
"sku": {
"family": "A",
"name": "Standard"
},
"tenantId": "d6019544-c403-415c-8e96-50009635b6aa",
"accessPolicies": [],
"enabledForDeployment": true,
"enabledForDiskEncryption": true,
"enabledForTemplateDeployment": true
},
"apiVersion": "2016-10-01"
}
]
}
}
}
],
"metadata": {
"_dependencyType": "secrets.keyVault"
}
}

View File

@@ -5,7 +5,9 @@
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "swagger", "launchUrl": "swagger",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development",
"VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/",
"AZURE_USERNAME": "Hauth@live.com"
}, },
"applicationUrl": "https://localhost:5001;http://localhost:5000" "applicationUrl": "https://localhost:5001;http://localhost:5000"
} }

View File

@@ -3,6 +3,11 @@
"identityapp1": { "identityapp1": {
"type": "identityapp", "type": "identityapp",
"dynamicId": null "dynamicId": null
},
"secrets1": {
"type": "secrets",
"connectionId": "VaultUri",
"dynamicId": null
} }
} }
} }

View File

@@ -3,6 +3,13 @@
"identityapp1": { "identityapp1": {
"type": "identityapp.default", "type": "identityapp.default",
"dynamicId": null "dynamicId": null
},
"secrets1": {
"secretStore": null,
"resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.KeyVault/vaults/GameboardShogiUISocketsv",
"type": "secrets.keyVault",
"connectionId": "VaultUri",
"dynamicId": null
} }
} }
} }

View File

@@ -0,0 +1,4 @@
# Gameboard.ShogiUI.Sockets
# Forgetmenots
Don't forget to run `dotnet user-secrets init` within the AAT project.

View File

@@ -67,12 +67,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
* 3) User document of Player2. * 3) User document of Player2.
*/ */
var session = group.FirstOrDefault()?.doc.ToObject<SessionDocument>(); var session = group.FirstOrDefault()?.doc.ToObject<SessionDocument>();
var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject<UserDocument>(); var player1 = group.Skip(1).FirstOrDefault()?.doc.ToObject<UserDocument>();
var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject<UserDocument>(); var player2Doc = group.Skip(2).FirstOrDefault()?.doc;
if (session != null && player1Doc != null) if (session != null && player1 != null && player2Doc != null)
{ {
var player2 = player2Doc == null ? null : new Models.User(player2Doc); var player2 = IsUserDocument(player2Doc)
sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1Doc.Id, player2?.Id)); ? new Models.User(player2Doc.ToObject<UserDocument>()!)
: null;
sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id));
} }
} }
return new Collection<SessionMetadata>(sessions); return new Collection<SessionMetadata>(sessions);
@@ -80,6 +82,11 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return new Collection<SessionMetadata>(Array.Empty<SessionMetadata>()); return new Collection<SessionMetadata>(Array.Empty<SessionMetadata>());
} }
private static bool IsUserDocument(JObject player2Doc)
{
return player2Doc?.SelectToken(nameof(CouchDocument.DocumentType))?.Value<WhichDocumentType>() == WhichDocumentType.User;
}
public async Task<Session?> ReadSession(string name) public async Task<Session?> ReadSession(string name)
{ {
static Shogi.Domain.Pieces.Piece? MapPiece(Piece? piece) static Shogi.Domain.Pieces.Piece? MapPiece(Piece? piece)
@@ -112,17 +119,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
* Everything Else) Snapshots of the boardstate after every player move. * Everything Else) Snapshots of the boardstate after every player move.
*/ */
var session = group[0].doc.ToObject<SessionDocument>(); var session = group[0].doc.ToObject<SessionDocument>();
var player1Doc = group[1].doc.ToObject<UserDocument>(); var player1 = group[1].doc.ToObject<UserDocument>();
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>(); var player2Doc = group[2].doc;
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
? group[2].doc.ToObject<UserDocument>()
: null;
var boardState = group.Last().doc.ToObject<BoardStateDocument>(); var boardState = group.Last().doc.ToObject<BoardStateDocument>();
if (session != null && player1Doc != null && boardState != null) if (session != null && player1 != null && boardState != null)
{ {
var player2 = player2Doc == null ? null : new Models.User(player2Doc); var player2 = IsUserDocument(player2Doc)
var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1Doc.DisplayName, player2Doc?.DisplayName); ? new Models.User(player2Doc.ToObject<UserDocument>()!)
: null;
var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id);
var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value))); var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value)));
return new Session(shogiBoardState, metaData); return new Session(shogiBoardState, metaData);
} }
@@ -151,15 +157,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
* 3) User document of Player2. * 3) User document of Player2.
*/ */
var session = group[0].doc.ToObject<SessionDocument>(); var session = group[0].doc.ToObject<SessionDocument>();
var player1Doc = group[1].doc.ToObject<UserDocument>(); var player1 = group[1].doc.ToObject<UserDocument>();
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>(); var player2Doc = group[2].doc;
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() if (session != null && player1 != null)
? group[2].doc.ToObject<UserDocument>()
: null;
if (session != null && player1Doc != null)
{ {
var player2 = player2Doc == null ? null : new Models.User(player2Doc); var player2 = IsUserDocument(player2Doc)
return new SessionMetadata(session.Name, session.IsPrivate, player1Doc.Id, player2?.Id); ? new Models.User(player2Doc.ToObject<UserDocument>()!)
: null;
return new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id);
} }
} }
return null; return null;

View File

@@ -18,5 +18,13 @@
"ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff", "ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff",
"SwaggerUIClientId": "26bf69a4-2af8-4711-bf5b-79f75e20b082" "SwaggerUIClientId": "26bf69a4-2af8-4711-bf5b-79f75e20b082"
}, },
"Cors": {
"AllowedOrigins": [
"http://localhost:3000",
"https://localhost:3000",
"https://api.lucaserver.space",
"https://lucaserver.space"
]
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -1,16 +1,26 @@
using Shogi.AcceptanceTests.TestSetup;
using Xunit.Abstractions;
namespace Shogi.AcceptanceTests namespace Shogi.AcceptanceTests
{ {
public class AcceptanceTests public class AcceptanceTests : IClassFixture<AATFixture>
{
public AcceptanceTests()
{ {
private readonly AATFixture fixture;
private readonly ITestOutputHelper console;
public AcceptanceTests(AATFixture fixture, ITestOutputHelper console)
{
this.fixture = fixture;
this.console = console;
} }
[Fact] [Fact]
public void CreateAndReadSession() public async Task CreateAndReadSession()
{ {
var response = await fixture.Service.GetAsync(new Uri("Game", UriKind.Relative));
console.WriteLine(await response.Content.ReadAsStringAsync());
console.WriteLine(response.Headers.WwwAuthenticate.ToString());
response.IsSuccessStatusCode.Should().BeTrue(because: "AAT Client should be authorized.");
} }
} }
} }

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
@@ -6,12 +6,31 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<UserSecretsId>96d6281d-a75b-4181-b535-ea34b26dc8a2</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.44.0" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@@ -0,0 +1,77 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client;
using System.Net.Http.Headers;
namespace Shogi.AcceptanceTests.TestSetup
{
public class AATFixture : IAsyncLifetime, IDisposable
{
private bool disposedValue;
private readonly IConfidentialClientApplication app;
public AATFixture()
{
Configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.AddUserSecrets<AATFixture>()
.Build();
var azure = Configuration.GetSection("Auth");
app = ConfidentialClientApplicationBuilder.Create(azure["ClientId"])
.WithTenantId(azure["TenantId"])
.WithClientSecret(azure["SecretValue"])
.Build();
Service = new HttpClient
{
BaseAddress = new Uri(Configuration["ServiceUrl"], UriKind.Absolute)
};
}
public IConfiguration Configuration { get; private set; }
public HttpClient Service { get; }
public async Task InitializeAsync()
{
var authResult = await app
.AcquireTokenForClient(Configuration.GetSection("Auth:Scopes").Get<string[]>())
.ExecuteAsync();
authResult.Should().NotBeNull();
authResult.AccessToken.Should().NotBeNullOrEmpty();
Service.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
var response = await Service.GetAsync("Socket/Token");
response.IsSuccessStatusCode.Should().BeTrue(because: "AAT client should create an account for tests.");
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
Service.Dispose();
}
disposedValue = true;
}
}
public Task DisposeAsync()
{
Dispose(true);
return Task.CompletedTask;
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -1 +1,2 @@
global using Xunit; global using Xunit;
global using FluentAssertions;

View File

@@ -0,0 +1,11 @@
{
"ServiceUrl": "https://localhost:5001",
"Auth": {
"TenantId": "d6019544-c403-415c-8e96-50009635b6aa",
"ClientId": "78b12a47-440c-4cc7-9402-f573a2802951",
"SecretValue": "REDACTED",
"Scopes": [
"api://c1e94676-cab0-42ba-8b6c-9532b8486fff/.default"
]
}
}