From 770344422df5118fe48e98c18ed3317a974b5cdd Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 19 Jun 2022 17:35:33 -0500 Subject: [PATCH] Scaffold some AAT stuff --- .../Controllers/SocketController.cs | 6 -- .../Gameboard.ShogiUI.Sockets.csproj | 3 + .../Managers/SocketTokenCache.cs | 2 +- Gameboard.ShogiUI.Sockets/Program.cs | 24 +++--- .../local/secrets1.arm.json | 76 ++++++++++++++++++ .../Properties/launchSettings.json | 4 +- .../Properties/serviceDependencies.json | 5 ++ .../Properties/serviceDependencies.local.json | 7 ++ Gameboard.ShogiUI.Sockets/Readme.md | 4 + .../Repositories/GameboardRepository.cs | 47 ++++++----- Gameboard.ShogiUI.Sockets/appsettings.json | 8 ++ Shogi.AcceptanceTests/AcceptanceTests.cs | 20 +++-- .../Shogi.AcceptanceTests.csproj | 25 +++++- Shogi.AcceptanceTests/TestSetup/AATFixture.cs | 77 +++++++++++++++++++ Shogi.AcceptanceTests/Usings.cs | 3 +- Shogi.AcceptanceTests/appsettings.json | 11 +++ 16 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 Gameboard.ShogiUI.Sockets/Properties/ServiceDependencies/local/secrets1.arm.json create mode 100644 Gameboard.ShogiUI.Sockets/Readme.md create mode 100644 Shogi.AcceptanceTests/TestSetup/AATFixture.cs create mode 100644 Shogi.AcceptanceTests/appsettings.json diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index 06b4657..1786b8e 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -5,12 +5,8 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Api; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System; using System.Security.Claims; -using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Controllers { @@ -19,7 +15,6 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [Authorize] public class SocketController : ControllerBase { - private readonly ILogger logger; private readonly ISocketTokenCache tokenCache; private readonly IGameboardManager gameboardManager; private readonly IGameboardRepository gameboardRepository; @@ -33,7 +28,6 @@ namespace Gameboard.ShogiUI.Sockets.Controllers IGameboardRepository gameboardRepository, ISocketConnectionManager connectionManager) { - this.logger = logger; this.tokenCache = tokenCache; this.gameboardManager = gameboardManager; this.gameboardRepository = gameboardRepository; diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index 973c0c2..cb5ed71 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -8,9 +8,12 @@ False False enable + 973a1f5f-ef25-4f1c-a24d-b0fc7d016ab8 + + diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs index 722dc33..7f6bead 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs @@ -35,7 +35,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers { await Task.Delay(TimeSpan.FromMinutes(1)); Tokens.Remove(userName, out _); - }); + }).ConfigureAwait(false); return guid; } diff --git a/Gameboard.ShogiUI.Sockets/Program.cs b/Gameboard.ShogiUI.Sockets/Program.cs index b20a9aa..db0ad07 100644 --- a/Gameboard.ShogiUI.Sockets/Program.cs +++ b/Gameboard.ShogiUI.Sockets/Program.cs @@ -60,16 +60,22 @@ namespace Gameboard.ShogiUI.Sockets { // TODO: Figure out how to make a middleware for sockets? var socketService = app.Services.GetRequiredService(); - - 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 allowedOrigins = app.Configuration.GetSection("Cors:AllowedOrigins").Get(); + var socketOptions = new WebSocketOptions(); - foreach (var o in origins) - socketOptions.AllowedOrigins.Add(o); - app.UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials()); + foreach (var origin in allowedOrigins) + socketOptions.AllowedOrigins.Add(origin); + + app.UseCors(options => + { + options + .WithOrigins(allowedOrigins) + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyMethod() + .AllowAnyHeader() + .WithExposedHeaders("Set-Cookie") + .AllowCredentials(); + }); app.UseWebSockets(socketOptions); app.Use(async (context, next) => { diff --git a/Gameboard.ShogiUI.Sockets/Properties/ServiceDependencies/local/secrets1.arm.json b/Gameboard.ShogiUI.Sockets/Properties/ServiceDependencies/local/secrets1.arm.json new file mode 100644 index 0000000..391cd0d --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Properties/ServiceDependencies/local/secrets1.arm.json @@ -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" + } +} \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json index 90e2237..0851a5a 100644 --- a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json +++ b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json @@ -5,7 +5,9 @@ "launchBrowser": true, "launchUrl": "swagger", "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" } diff --git a/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.json b/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.json index 44cc45e..2e47b3f 100644 --- a/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.json +++ b/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.json @@ -3,6 +3,11 @@ "identityapp1": { "type": "identityapp", "dynamicId": null + }, + "secrets1": { + "type": "secrets", + "connectionId": "VaultUri", + "dynamicId": null } } } \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.local.json b/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.local.json index 3c85224..cb2adfb 100644 --- a/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.local.json +++ b/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.local.json @@ -3,6 +3,13 @@ "identityapp1": { "type": "identityapp.default", "dynamicId": null + }, + "secrets1": { + "secretStore": null, + "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.KeyVault/vaults/GameboardShogiUISocketsv", + "type": "secrets.keyVault", + "connectionId": "VaultUri", + "dynamicId": null } } } \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Readme.md b/Gameboard.ShogiUI.Sockets/Readme.md new file mode 100644 index 0000000..cc7e22c --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Readme.md @@ -0,0 +1,4 @@ +# Gameboard.ShogiUI.Sockets + +# Forgetmenots +Don't forget to run `dotnet user-secrets init` within the AAT project. \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index 0022591..2f932c7 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -67,12 +67,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories * 3) User document of Player2. */ var session = group.FirstOrDefault()?.doc.ToObject(); - var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject(); - var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject(); - if (session != null && player1Doc != null) + var player1 = group.Skip(1).FirstOrDefault()?.doc.ToObject(); + var player2Doc = group.Skip(2).FirstOrDefault()?.doc; + if (session != null && player1 != null && player2Doc != null) { - var player2 = player2Doc == null ? null : new Models.User(player2Doc); - sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1Doc.Id, player2?.Id)); + var player2 = IsUserDocument(player2Doc) + ? new Models.User(player2Doc.ToObject()!) + : null; + sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id)); } } return new Collection(sessions); @@ -80,6 +82,11 @@ namespace Gameboard.ShogiUI.Sockets.Repositories return new Collection(Array.Empty()); } + private static bool IsUserDocument(JObject player2Doc) + { + return player2Doc?.SelectToken(nameof(CouchDocument.DocumentType))?.Value() == WhichDocumentType.User; + } + public async Task ReadSession(string name) { 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. */ var session = group[0].doc.ToObject(); - var player1Doc = group[1].doc.ToObject(); - var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); - var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() - ? group[2].doc.ToObject() - : null; + var player1 = group[1].doc.ToObject(); + var player2Doc = group[2].doc; var boardState = group.Last().doc.ToObject(); - if (session != null && player1Doc != null && boardState != null) + if (session != null && player1 != null && boardState != null) { - var player2 = player2Doc == null ? null : new Models.User(player2Doc); - var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1Doc.DisplayName, player2Doc?.DisplayName); + var player2 = IsUserDocument(player2Doc) + ? new Models.User(player2Doc.ToObject()!) + : 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))); return new Session(shogiBoardState, metaData); } @@ -151,15 +157,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories * 3) User document of Player2. */ var session = group[0].doc.ToObject(); - var player1Doc = group[1].doc.ToObject(); - var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); - var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() - ? group[2].doc.ToObject() - : null; - if (session != null && player1Doc != null) + var player1 = group[1].doc.ToObject(); + var player2Doc = group[2].doc; + if (session != null && player1 != null) { - var player2 = player2Doc == null ? null : new Models.User(player2Doc); - return new SessionMetadata(session.Name, session.IsPrivate, player1Doc.Id, player2?.Id); + var player2 = IsUserDocument(player2Doc) + ? new Models.User(player2Doc.ToObject()!) + : null; + return new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id); } } return null; diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json index 7b37247..87c4844 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -18,5 +18,13 @@ "ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff", "SwaggerUIClientId": "26bf69a4-2af8-4711-bf5b-79f75e20b082" }, + "Cors": { + "AllowedOrigins": [ + "http://localhost:3000", + "https://localhost:3000", + "https://api.lucaserver.space", + "https://lucaserver.space" + ] + }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/Shogi.AcceptanceTests/AcceptanceTests.cs b/Shogi.AcceptanceTests/AcceptanceTests.cs index b407d7a..d75ea58 100644 --- a/Shogi.AcceptanceTests/AcceptanceTests.cs +++ b/Shogi.AcceptanceTests/AcceptanceTests.cs @@ -1,16 +1,26 @@ +using Shogi.AcceptanceTests.TestSetup; +using Xunit.Abstractions; + namespace Shogi.AcceptanceTests { - public class AcceptanceTests + public class AcceptanceTests : IClassFixture { - public AcceptanceTests() - { + private readonly AATFixture fixture; + private readonly ITestOutputHelper console; + public AcceptanceTests(AATFixture fixture, ITestOutputHelper console) + { + this.fixture = fixture; + this.console = console; } [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."); } } } \ No newline at end of file diff --git a/Shogi.AcceptanceTests/Shogi.AcceptanceTests.csproj b/Shogi.AcceptanceTests/Shogi.AcceptanceTests.csproj index c5d1063..554316d 100644 --- a/Shogi.AcceptanceTests/Shogi.AcceptanceTests.csproj +++ b/Shogi.AcceptanceTests/Shogi.AcceptanceTests.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -6,12 +6,31 @@ enable false + 96d6281d-a75b-4181-b535-ea34b26dc8a2 - + + + + + + PreserveNewest + + + + + + + + + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Shogi.AcceptanceTests/TestSetup/AATFixture.cs b/Shogi.AcceptanceTests/TestSetup/AATFixture.cs new file mode 100644 index 0000000..719751c --- /dev/null +++ b/Shogi.AcceptanceTests/TestSetup/AATFixture.cs @@ -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() + .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()) + .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); + } + } +} diff --git a/Shogi.AcceptanceTests/Usings.cs b/Shogi.AcceptanceTests/Usings.cs index 8c927eb..7fef4b0 100644 --- a/Shogi.AcceptanceTests/Usings.cs +++ b/Shogi.AcceptanceTests/Usings.cs @@ -1 +1,2 @@ -global using Xunit; \ No newline at end of file +global using Xunit; +global using FluentAssertions; \ No newline at end of file diff --git a/Shogi.AcceptanceTests/appsettings.json b/Shogi.AcceptanceTests/appsettings.json new file mode 100644 index 0000000..91c3d65 --- /dev/null +++ b/Shogi.AcceptanceTests/appsettings.json @@ -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" + ] + } +} \ No newline at end of file