massive checkpoint
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
17
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs
Normal file
17
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||
{
|
||||
public class GetGameResponse
|
||||
{
|
||||
public Game Game { get; set; }
|
||||
public WhichPlayer PlayerPerspective { get; set; }
|
||||
public BoardState BoardState { get; set; }
|
||||
public IList<Move> MoveHistory { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,6 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||
{
|
||||
public class PostMove
|
||||
{
|
||||
[Required]
|
||||
public string GameName { get; set; }
|
||||
|
||||
[Required]
|
||||
public Move Move { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
public class PostSession
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Player1 { get; set; }
|
||||
public string Player2 { get; set; }
|
||||
public bool IsPrivate { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using System;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||
{
|
||||
public class CreateGameRequest : IRequest
|
||||
{
|
||||
public ClientAction Action { get; set; }
|
||||
public string GameName { get; set; } = string.Empty;
|
||||
public bool IsPrivate { get; set; }
|
||||
}
|
||||
|
||||
public class CreateGameResponse : IResponse
|
||||
{
|
||||
public string Action { get; }
|
||||
public string Error { get; set; }
|
||||
public Game Game { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player who created the game.
|
||||
/// </summary>
|
||||
public string PlayerName { get; set; }
|
||||
|
||||
public CreateGameResponse()
|
||||
{
|
||||
Action = ClientAction.CreateGame.ToString();
|
||||
Error = string.Empty;
|
||||
Game = new Game();
|
||||
PlayerName = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
public interface IResponse
|
||||
{
|
||||
string Action { get; }
|
||||
string Error { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||
{
|
||||
public class ListGamesRequest : IRequest
|
||||
{
|
||||
public ClientAction Action { get; set; }
|
||||
}
|
||||
|
||||
public class ListGamesResponse : IResponse
|
||||
{
|
||||
public string Action { get; }
|
||||
public string Error { get; set; }
|
||||
public IReadOnlyList<Game> Games { get; set; }
|
||||
|
||||
public ListGamesResponse()
|
||||
{
|
||||
Action = ClientAction.ListGames.ToString();
|
||||
Error = "";
|
||||
Games = new Collection<Game>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||
{
|
||||
public class LoadGameRequest : IRequest
|
||||
{
|
||||
public ClientAction Action { get; set; }
|
||||
public string GameName { get; set; } = "";
|
||||
}
|
||||
|
||||
public class LoadGameResponse : IResponse
|
||||
{
|
||||
public string Action { get; }
|
||||
public Game Game { get; set; }
|
||||
public WhichPlayer PlayerPerspective { get; set; }
|
||||
public BoardState BoardState { get; set; }
|
||||
public IList<Move> MoveHistory { get; set; }
|
||||
public string Error { get; set; }
|
||||
|
||||
public LoadGameResponse()
|
||||
{
|
||||
Action = ClientAction.LoadGame.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,19 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||
{
|
||||
public class MoveRequest : IRequest
|
||||
{
|
||||
public ClientAction Action { get; set; }
|
||||
public string GameName { get; set; } = string.Empty;
|
||||
public Move Move { get; set; } = new Move();
|
||||
}
|
||||
|
||||
public class MoveResponse : IResponse
|
||||
{
|
||||
public string Action { get; }
|
||||
public string Error { get; set; }
|
||||
public string GameName { get; set; }
|
||||
public string PlayerName { get; set; }
|
||||
public Move Move { get; set; }
|
||||
public string Action { get; protected set; }
|
||||
public Game Game { get; set; }
|
||||
public WhichPlayer PlayerPerspective { get; set; }
|
||||
public BoardState BoardState { get; set; }
|
||||
public IList<Move> MoveHistory { get; set; }
|
||||
|
||||
public MoveResponse()
|
||||
{
|
||||
Action = ClientAction.Move.ToString();
|
||||
Error = string.Empty;
|
||||
GameName = string.Empty;
|
||||
PlayerName = string.Empty;
|
||||
Move = new Move();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
{
|
||||
public enum ClientAction
|
||||
{
|
||||
ListGames,
|
||||
CreateGame,
|
||||
JoinGame,
|
||||
JoinByCode,
|
||||
LoadGame,
|
||||
Move
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Gameboard.ShogiUI.Sockets.Managers;
|
||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||
using Gameboard.ShogiUI.Sockets.Managers;
|
||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
@@ -10,16 +12,15 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
[Authorize(Roles = "Shogi")]
|
||||
public class GameController : ControllerBase
|
||||
{
|
||||
private static readonly string UsernameClaim = "preferred_username";
|
||||
private readonly IGameboardManager gameboardManager;
|
||||
private readonly IGameboardRepository gameboardRepository;
|
||||
private readonly ISocketConnectionManager communicationManager;
|
||||
private string? JwtUserName => HttpContext.User.Claims.FirstOrDefault(c => c.Type == UsernameClaim)?.Value;
|
||||
|
||||
public GameController(
|
||||
IGameboardRepository repository,
|
||||
@@ -68,22 +69,12 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
[HttpPost("{gameName}/Move")]
|
||||
public async Task<IActionResult> PostMove([FromRoute] string gameName, [FromBody] PostMove request)
|
||||
{
|
||||
Models.User? user = null;
|
||||
if (Request.Cookies.ContainsKey(SocketController.WebSessionKey))
|
||||
{
|
||||
var webSessionId = Guid.Parse(Request.Cookies[SocketController.WebSessionKey]!);
|
||||
user = await gameboardManager.ReadUser(webSessionId);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(JwtUserName))
|
||||
{
|
||||
user = await gameboardManager.ReadUser(JwtUserName);
|
||||
}
|
||||
|
||||
var session = await gameboardManager.ReadSession(gameName);
|
||||
var user = await gameboardManager.ReadUser(User);
|
||||
var session = await gameboardRepository.ReadSession(gameName);
|
||||
|
||||
if (session == null || user == null || (session.Player1 != user.Name && session.Player2 != user.Name))
|
||||
{
|
||||
throw new UnauthorizedAccessException("You are not seated at this game.");
|
||||
throw new UnauthorizedAccessException("User is not seated at this game.");
|
||||
}
|
||||
|
||||
var move = request.Move;
|
||||
@@ -94,13 +85,19 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
|
||||
if (moveSuccess)
|
||||
{
|
||||
var createSuccess = await gameboardRepository.CreateBoardState(session);
|
||||
if (!createSuccess)
|
||||
{
|
||||
throw new ApplicationException("Unable to persist board state.");
|
||||
}
|
||||
await communicationManager.BroadcastToPlayers(new MoveResponse
|
||||
{
|
||||
GameName = session.Name,
|
||||
PlayerName = user.Name,
|
||||
Move = moveModel.ToServiceModel()
|
||||
BoardState = session.Shogi.ToServiceModel(),
|
||||
Game = session.ToServiceModel(),
|
||||
MoveHistory = session.Shogi.MoveHistory.Select(h => h.ToServiceModel()).ToList(),
|
||||
PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2
|
||||
}, session.Player1, session.Player2);
|
||||
return Created(string.Empty, null);
|
||||
return Ok();
|
||||
}
|
||||
throw new InvalidOperationException("Illegal move.");
|
||||
}
|
||||
@@ -124,5 +121,56 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
// }
|
||||
// return new ConflictResult();
|
||||
//}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
||||
{
|
||||
var user = await gameboardManager.ReadUser(User);
|
||||
var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!.Name);
|
||||
var success = await gameboardRepository.CreateSession(session);
|
||||
|
||||
if (success)
|
||||
{
|
||||
await communicationManager.BroadcastToAll(new CreateGameResponse
|
||||
{
|
||||
Game = session.ToServiceModel(),
|
||||
PlayerName = user.Name
|
||||
});
|
||||
return Ok();
|
||||
}
|
||||
return Conflict();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the board session and subscribes the caller to socket events for that session.
|
||||
/// </summary>
|
||||
[HttpGet("{gameName}")]
|
||||
public async Task<IActionResult> GetSession([FromRoute] string gameName)
|
||||
{
|
||||
var user = await gameboardManager.ReadUser(User);
|
||||
var session = await gameboardRepository.ReadSession(gameName);
|
||||
if (session == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
communicationManager.SubscribeToGame(session, user!.Name);
|
||||
var response = new GetGameResponse()
|
||||
{
|
||||
Game = new Models.SessionMetadata(session).ToServiceModel(),
|
||||
BoardState = session.Shogi.ToServiceModel(),
|
||||
MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(),
|
||||
PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2
|
||||
};
|
||||
return new JsonResult(response);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetSessions()
|
||||
{
|
||||
var sessions = await gameboardRepository.ReadSessionMetadatas();
|
||||
return new JsonResult(sessions.Select(s => s.ToServiceModel()).ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,113 @@
|
||||
using Gameboard.ShogiUI.Sockets.Managers;
|
||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||
using Gameboard.ShogiUI.Sockets.Managers;
|
||||
using Gameboard.ShogiUI.Sockets.Models;
|
||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||
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.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Route("[controller]")]
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
[Authorize(Roles = "Shogi")]
|
||||
public class SocketController : ControllerBase
|
||||
{
|
||||
public static readonly string WebSessionKey = "session-id";
|
||||
private readonly ILogger<SocketController> logger;
|
||||
private readonly ISocketTokenManager tokenManager;
|
||||
private readonly ISocketTokenCache tokenCache;
|
||||
private readonly IGameboardManager gameboardManager;
|
||||
private readonly IGameboardRepository gameboardRepository;
|
||||
private readonly CookieOptions createSessionOptions;
|
||||
private readonly CookieOptions deleteSessionOptions;
|
||||
private readonly AuthenticationProperties authenticationProps;
|
||||
|
||||
public SocketController(
|
||||
ILogger<SocketController> logger,
|
||||
ISocketTokenManager tokenManager,
|
||||
ISocketTokenCache tokenCache,
|
||||
IGameboardManager gameboardManager,
|
||||
IGameboardRepository gameboardRepository)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.tokenManager = tokenManager;
|
||||
this.tokenCache = tokenCache;
|
||||
this.gameboardManager = gameboardManager;
|
||||
this.gameboardRepository = gameboardRepository;
|
||||
createSessionOptions = new CookieOptions
|
||||
authenticationProps = new AuthenticationProperties
|
||||
{
|
||||
Secure = true,
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.None,
|
||||
Expires = DateTimeOffset.Now.AddYears(5)
|
||||
AllowRefresh = true,
|
||||
IsPersistent = true
|
||||
};
|
||||
deleteSessionOptions = new CookieOptions();
|
||||
}
|
||||
|
||||
[HttpGet("Yep")]
|
||||
[HttpGet("GuestLogout")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Yep()
|
||||
public async Task<IActionResult> GuestLogout()
|
||||
{
|
||||
deleteSessionOptions.Expires = DateTimeOffset.Now.AddDays(-1);
|
||||
Response.Cookies.Append(WebSessionKey, "", deleteSessionOptions);
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("Token")]
|
||||
public IActionResult GetToken()
|
||||
public async Task<IActionResult> GetToken()
|
||||
{
|
||||
var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
|
||||
var token = tokenManager.GenerateToken(userName);
|
||||
var identityId = User.UserId();
|
||||
if (string.IsNullOrWhiteSpace(identityId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var user = await gameboardManager.ReadUser(User);
|
||||
if (user == null)
|
||||
{
|
||||
user = new User(identityId);
|
||||
var success = await gameboardRepository.CreateUser(user);
|
||||
if (!success)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
var token = tokenCache.GenerateToken(user.Name);
|
||||
return new JsonResult(new GetTokenResponse(token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a token for guest users to send when requesting a socket connection.
|
||||
/// Sends a HttpOnly cookie to the client with which to identify guest users.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("GuestToken")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> GetGuestToken()
|
||||
{
|
||||
var cookies = Request.Cookies;
|
||||
var webSessionId = cookies.ContainsKey(WebSessionKey)
|
||||
? Guid.Parse(cookies[WebSessionKey]!)
|
||||
: Guid.NewGuid();
|
||||
var webSessionIdAsString = webSessionId.ToString();
|
||||
|
||||
if (Guid.TryParse(User.UserId(), out Guid webSessionId))
|
||||
{
|
||||
var user = await gameboardRepository.ReadGuestUser(webSessionId);
|
||||
if (user == null)
|
||||
if (user != null)
|
||||
{
|
||||
var userName = await gameboardManager.CreateGuestUser(webSessionId);
|
||||
var token = tokenManager.GenerateToken(webSessionIdAsString);
|
||||
Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions);
|
||||
return new JsonResult(new GetGuestTokenResponse(userName, token));
|
||||
}
|
||||
else
|
||||
{
|
||||
var token = tokenManager.GenerateToken(webSessionIdAsString);
|
||||
Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions);
|
||||
var token = tokenCache.GenerateToken(webSessionId.ToString());
|
||||
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Setup a guest user.
|
||||
var newSessionId = Guid.NewGuid();
|
||||
var user = new User(Guid.NewGuid().ToString(), newSessionId);
|
||||
if (await gameboardRepository.CreateUser(user))
|
||||
{
|
||||
var identity = user.CreateGuestUserIdentity();
|
||||
await this.HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(identity),
|
||||
authenticationProps
|
||||
);
|
||||
|
||||
var token = tokenCache.GenerateToken(newSessionId.ToString());
|
||||
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
|
||||
}
|
||||
}
|
||||
|
||||
return Unauthorized();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
Normal file
18
Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Extensions
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string? UserId(this ClaimsPrincipal self)
|
||||
{
|
||||
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
}
|
||||
|
||||
public static bool IsGuest(this ClaimsPrincipal self)
|
||||
{
|
||||
return self.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Guest");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="10.3.0" />
|
||||
<PackageReference Include="IdentityModel" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="1.5.1" />
|
||||
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.5.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="FluentValidation" Version="10.3.3" />
|
||||
<PackageReference Include="IdentityModel" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="5.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.9" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="1.16.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.16.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NSwag.AspNetCore" Version="13.13.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
using Gameboard.ShogiUI.Sockets.Models;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
||||
{
|
||||
public interface ICreateGameHandler
|
||||
{
|
||||
Task Handle(CreateGameRequest request, string userName);
|
||||
}
|
||||
|
||||
// TODO: This doesn't need to be a socket action.
|
||||
// It can be an API route and still tell socket connections about the new session.
|
||||
public class CreateGameHandler : ICreateGameHandler
|
||||
{
|
||||
private readonly IGameboardManager manager;
|
||||
private readonly ISocketConnectionManager connectionManager;
|
||||
|
||||
public CreateGameHandler(
|
||||
ISocketConnectionManager communicationManager,
|
||||
IGameboardManager manager)
|
||||
{
|
||||
this.manager = manager;
|
||||
this.connectionManager = communicationManager;
|
||||
}
|
||||
|
||||
public async Task Handle(CreateGameRequest request, string userName)
|
||||
{
|
||||
var model = new SessionMetadata(request.GameName, request.IsPrivate, userName, null);
|
||||
var success = await manager.CreateSession(model);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
var error = new CreateGameResponse()
|
||||
{
|
||||
Error = "Unable to create game with this name."
|
||||
};
|
||||
await connectionManager.BroadcastToPlayers(error, userName);
|
||||
}
|
||||
|
||||
var response = new CreateGameResponse()
|
||||
{
|
||||
PlayerName = userName,
|
||||
Game = model.ToServiceModel()
|
||||
};
|
||||
|
||||
var task = request.IsPrivate
|
||||
? connectionManager.BroadcastToPlayers(response, userName)
|
||||
: connectionManager.BroadcastToAll(response);
|
||||
|
||||
await task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
||||
{
|
||||
public interface IListGamesHandler
|
||||
{
|
||||
Task Handle(ListGamesRequest request, string userName);
|
||||
}
|
||||
|
||||
public class ListGamesHandler : IListGamesHandler
|
||||
{
|
||||
private readonly ISocketConnectionManager communicationManager;
|
||||
private readonly IGameboardRepository repository;
|
||||
|
||||
public ListGamesHandler(
|
||||
ISocketConnectionManager communicationManager,
|
||||
IGameboardRepository repository)
|
||||
{
|
||||
this.communicationManager = communicationManager;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public async Task Handle(ListGamesRequest _, string userName)
|
||||
{
|
||||
var sessions = await repository.ReadSessionMetadatas();
|
||||
var games = sessions.Select(s => new Game(s.Name, s.Player1, s.Player2)).ToList();
|
||||
|
||||
var response = new ListGamesResponse()
|
||||
{
|
||||
Games = games
|
||||
};
|
||||
|
||||
await communicationManager.BroadcastToPlayers(response, userName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using Gameboard.ShogiUI.Sockets.Models;
|
||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
||||
{
|
||||
public interface ILoadGameHandler
|
||||
{
|
||||
Task Handle(LoadGameRequest request, string userName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes a user to messages for a session and loads that session into the BoardManager for playing.
|
||||
/// </summary>
|
||||
public class LoadGameHandler : ILoadGameHandler
|
||||
{
|
||||
private readonly ILogger<LoadGameHandler> logger;
|
||||
private readonly IGameboardRepository gameboardRepository;
|
||||
private readonly ISocketConnectionManager communicationManager;
|
||||
|
||||
public LoadGameHandler(
|
||||
ILogger<LoadGameHandler> logger,
|
||||
ISocketConnectionManager communicationManager,
|
||||
IGameboardRepository gameboardRepository)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.gameboardRepository = gameboardRepository;
|
||||
this.communicationManager = communicationManager;
|
||||
}
|
||||
|
||||
public async Task Handle(LoadGameRequest request, string userName)
|
||||
{
|
||||
var sessionModel = await gameboardRepository.ReadSession(request.GameName);
|
||||
if (sessionModel == null)
|
||||
{
|
||||
logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName);
|
||||
var error = new LoadGameResponse() { Error = "Game not found." };
|
||||
await communicationManager.BroadcastToPlayers(error, userName);
|
||||
return;
|
||||
}
|
||||
|
||||
communicationManager.SubscribeToGame(sessionModel, userName);
|
||||
|
||||
var response = new LoadGameResponse()
|
||||
{
|
||||
Game = new SessionMetadata(sessionModel).ToServiceModel(),
|
||||
BoardState = sessionModel.Shogi.ToServiceModel(),
|
||||
MoveHistory = sessionModel.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList()
|
||||
};
|
||||
await communicationManager.BroadcastToPlayers(response, userName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
||||
{
|
||||
public interface IMoveHandler
|
||||
{
|
||||
Task Handle(MoveRequest request, string userName);
|
||||
}
|
||||
public class MoveHandler : IMoveHandler
|
||||
{
|
||||
private readonly IGameboardManager gameboardManager;
|
||||
private readonly ISocketConnectionManager connectionManager;
|
||||
public MoveHandler(
|
||||
ISocketConnectionManager connectionManager,
|
||||
IGameboardManager gameboardManager)
|
||||
{
|
||||
this.gameboardManager = gameboardManager;
|
||||
this.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
public async Task Handle(MoveRequest request, string userName)
|
||||
{
|
||||
Models.Move moveModel;
|
||||
if (request.Move.PieceFromCaptured.HasValue)
|
||||
{
|
||||
moveModel = new Models.Move(request.Move.PieceFromCaptured.Value, request.Move.To);
|
||||
}
|
||||
else
|
||||
{
|
||||
moveModel = new Models.Move(request.Move.From!, request.Move.To, request.Move.IsPromotion);
|
||||
}
|
||||
|
||||
var session = await gameboardManager.ReadSession(request.GameName);
|
||||
if (session != null)
|
||||
{
|
||||
var shogi = session.Shogi;
|
||||
var moveSuccess = shogi.Move(moveModel);
|
||||
if (moveSuccess)
|
||||
{
|
||||
await gameboardManager.CreateBoardState(session.Name, shogi);
|
||||
var response = new MoveResponse()
|
||||
{
|
||||
GameName = request.GameName,
|
||||
PlayerName = userName,
|
||||
Move = moveModel.ToServiceModel()
|
||||
};
|
||||
await connectionManager.BroadcastToPlayers(response, session.Player1, session.Player2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = new MoveResponse()
|
||||
{
|
||||
Error = "Invalid move."
|
||||
};
|
||||
await connectionManager.BroadcastToPlayers(response, userName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,21 @@
|
||||
using Gameboard.ShogiUI.Sockets.Models;
|
||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||
using Gameboard.ShogiUI.Sockets.Models;
|
||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
{
|
||||
public interface IGameboardManager
|
||||
{
|
||||
Task<string> CreateGuestUser(Guid webSessionId);
|
||||
Task<bool> IsPlayer1(string sessionName, string playerName);
|
||||
Task<bool> CreateSession(SessionMetadata session);
|
||||
Task<Session?> ReadSession(string gameName);
|
||||
Task<bool> UpdateSession(SessionMetadata session);
|
||||
Task<bool> AssignPlayer2ToSession(string sessionName, string userName);
|
||||
Task<bool> CreateBoardState(string sessionName, Shogi shogi);
|
||||
Task<User?> ReadUser(string userName);
|
||||
Task<User?> ReadUser(Guid webSessionId);
|
||||
Task<User?> ReadUser(ClaimsPrincipal user);
|
||||
}
|
||||
|
||||
public class GameboardManager : IGameboardManager
|
||||
{
|
||||
private const int MaxTries = 3;
|
||||
private readonly IGameboardRepository repository;
|
||||
|
||||
public GameboardManager(IGameboardRepository repository)
|
||||
@@ -28,30 +23,21 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public async Task<string> CreateGuestUser(Guid webSessionId)
|
||||
public Task<User?> ReadUser(ClaimsPrincipal user)
|
||||
{
|
||||
var count = 0;
|
||||
while (count < MaxTries)
|
||||
{
|
||||
count++;
|
||||
var userName = $"Guest-{Guid.NewGuid()}";
|
||||
var isCreated = await repository.CreateUser(new User(userName, webSessionId));
|
||||
if (isCreated)
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
}
|
||||
throw new OperationCanceledException($"Failed to create guest user after {count} tries.");
|
||||
}
|
||||
|
||||
public Task<User?> ReadUser(Guid webSessionId)
|
||||
var userId = user.UserId();
|
||||
if (user.IsGuest() && Guid.TryParse(userId, out var webSessionId))
|
||||
{
|
||||
return repository.ReadGuestUser(webSessionId);
|
||||
}
|
||||
public Task<User?> ReadUser(string userName)
|
||||
else if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return repository.ReadUser(userName);
|
||||
return repository.ReadUser(userId);
|
||||
}
|
||||
|
||||
return Task.FromResult<User?>(null);
|
||||
}
|
||||
|
||||
public async Task<bool> IsPlayer1(string sessionName, string playerName)
|
||||
{
|
||||
//var session = await repository.GetGame(sessionName);
|
||||
@@ -69,31 +55,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public Task<bool> CreateSession(SessionMetadata session)
|
||||
{
|
||||
return repository.CreateSession(session);
|
||||
}
|
||||
|
||||
public Task<Session?> ReadSession(string sessionName)
|
||||
{
|
||||
return repository.ReadSession(sessionName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the session to storage.
|
||||
/// </summary>
|
||||
/// <param name="session">The session to save.</param>
|
||||
/// <returns>True if the session was saved successfully.</returns>
|
||||
public Task<bool> UpdateSession(SessionMetadata session)
|
||||
{
|
||||
return repository.UpdateSession(session);
|
||||
}
|
||||
|
||||
public Task<bool> CreateBoardState(string sessionName, Shogi shogi)
|
||||
{
|
||||
return repository.CreateBoardState(sessionName, shogi);
|
||||
}
|
||||
|
||||
public async Task<bool> AssignPlayer2ToSession(string sessionName, string userName)
|
||||
{
|
||||
var isSuccess = false;
|
||||
|
||||
@@ -6,20 +6,20 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
{
|
||||
public interface ISocketTokenManager
|
||||
public interface ISocketTokenCache
|
||||
{
|
||||
Guid GenerateToken(string s);
|
||||
string GetUsername(Guid g);
|
||||
string? GetUsername(Guid g);
|
||||
}
|
||||
|
||||
public class SocketTokenManager : ISocketTokenManager
|
||||
public class SocketTokenCache : ISocketTokenCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Key is userName or webSessionId
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, Guid> Tokens;
|
||||
|
||||
public SocketTokenManager()
|
||||
public SocketTokenCache()
|
||||
{
|
||||
Tokens = new ConcurrentDictionary<string, Guid>();
|
||||
}
|
||||
@@ -41,7 +41,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
||||
}
|
||||
|
||||
/// <returns>User name associated to the guid or null.</returns>
|
||||
public string GetUsername(Guid guid)
|
||||
public string? GetUsername(Guid guid)
|
||||
{
|
||||
var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key;
|
||||
if (userName != null)
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
@@ -30,5 +31,7 @@ namespace Gameboard.ShogiUI.Sockets.Models
|
||||
{
|
||||
Player2 = userName;
|
||||
}
|
||||
|
||||
public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1, Player2 = Player2 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
public string? Player2 { get; private set; }
|
||||
public bool IsPrivate { get; }
|
||||
|
||||
public SessionMetadata(string name, bool isPrivate, string player1, string? player2)
|
||||
public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null)
|
||||
{
|
||||
Name = name;
|
||||
IsPrivate = isPrivate;
|
||||
|
||||
@@ -1,18 +1,57 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Models
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public static readonly string GuestPrefix = "Guest-";
|
||||
public string Name { get; }
|
||||
public Guid? WebSessionId { get; }
|
||||
public bool IsGuest => Name.StartsWith(GuestPrefix) && WebSessionId.HasValue;
|
||||
|
||||
public User(string name, Guid? webSessionId = null)
|
||||
public bool IsGuest => WebSessionId.HasValue;
|
||||
|
||||
public User(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for guest user.
|
||||
/// </summary>
|
||||
public User(string name, Guid webSessionId)
|
||||
{
|
||||
Name = name;
|
||||
WebSessionId = webSessionId;
|
||||
}
|
||||
|
||||
public ClaimsIdentity CreateMsalUserIdentity()
|
||||
{
|
||||
var claims = new List<Claim>()
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, Name),
|
||||
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||
};
|
||||
return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
public ClaimsIdentity CreateGuestUserIdentity()
|
||||
{
|
||||
// TODO: Make this method static and factory-like.
|
||||
if (!WebSessionId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot create guest identity without a session identifier.");
|
||||
}
|
||||
|
||||
var claims = new List<Claim>()
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, WebSessionId.Value.ToString()),
|
||||
new Claim(ClaimTypes.Role, "Guest"),
|
||||
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||
};
|
||||
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
Normal file
8
Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.Models
|
||||
{
|
||||
public enum WhichLoginPlatform
|
||||
{
|
||||
Microsoft,
|
||||
Guest
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,12 @@
|
||||
},
|
||||
"Kestrel": {
|
||||
"commandName": "Project",
|
||||
"launchUrl": "Socket/Token",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "/swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://127.0.0.1:5100"
|
||||
"applicationUrl": "http://localhost:5100"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
using System;
|
||||
using Gameboard.ShogiUI.Sockets.Models;
|
||||
using System;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||
{
|
||||
public class UserDocument : CouchDocument
|
||||
{
|
||||
public enum LoginPlatform
|
||||
{
|
||||
Microsoft,
|
||||
Guest
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public LoginPlatform Platform { get; set; }
|
||||
public WhichLoginPlatform Platform { get; set; }
|
||||
/// <summary>
|
||||
/// The browser session ID saved via Set-Cookie headers.
|
||||
/// Only used with guest accounts.
|
||||
@@ -31,8 +27,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||
Name = name;
|
||||
WebSessionId = webSessionId;
|
||||
Platform = WebSessionId.HasValue
|
||||
? LoginPlatform.Guest
|
||||
: LoginPlatform.Microsoft;
|
||||
? WhichLoginPlatform.Guest
|
||||
: WhichLoginPlatform.Microsoft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
{
|
||||
public interface IGameboardRepository
|
||||
{
|
||||
Task<bool> CreateBoardState(string sessionName, Models.Shogi shogi);
|
||||
Task<bool> CreateBoardState(Models.Session session);
|
||||
Task<bool> CreateSession(Models.SessionMetadata session);
|
||||
Task<bool> CreateUser(Models.User user);
|
||||
Task<IList<Models.SessionMetadata>> ReadSessionMetadatas();
|
||||
@@ -46,13 +47,18 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson);
|
||||
var response = await client.PostAsync("_find", content);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
var sessions = JsonConvert.DeserializeObject<CouchFindResult<SessionDocument>>(responseContent).docs;
|
||||
|
||||
return sessions
|
||||
var results = JsonConvert.DeserializeObject<CouchFindResult<SessionDocument>>(responseContent);
|
||||
if (results != null)
|
||||
{
|
||||
return results
|
||||
.docs
|
||||
.Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return new List<Models.SessionMetadata>(0);
|
||||
}
|
||||
|
||||
public async Task<Models.Session?> ReadSession(string name)
|
||||
{
|
||||
var readShogiTask = ReadShogi(name);
|
||||
@@ -113,9 +119,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
return new Models.Shogi(moves);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateBoardState(string sessionName, Models.Shogi shogi)
|
||||
/// <summary>
|
||||
/// Saves a snapshot of board state and the most recent move.
|
||||
/// </summary>
|
||||
public async Task<bool> CreateBoardState(Models.Session session)
|
||||
{
|
||||
var boardStateDocument = new BoardStateDocument(sessionName, shogi);
|
||||
var boardStateDocument = new BoardStateDocument(session.Name, session.Shogi);
|
||||
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
||||
var response = await client.PostAsync(string.Empty, content);
|
||||
return response.IsSuccessStatusCode;
|
||||
@@ -197,9 +206,13 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
}
|
||||
|
||||
public async Task<Models.User?> ReadUser(string userName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = new UserDocument(userName);
|
||||
var response = await client.GetAsync(document.Id);
|
||||
var uri = new Uri(client.BaseAddress!, HttpUtility.UrlEncode(document.Id));
|
||||
var response = await client.GetAsync(HttpUtility.UrlEncode(document.Id));
|
||||
var response2 = await client.GetAsync(uri);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -207,6 +220,11 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||
|
||||
return new Models.User(user.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using FluentValidation;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
||||
{
|
||||
public class CreateGameRequestValidator : AbstractValidator<CreateGameRequest>
|
||||
{
|
||||
public CreateGameRequestValidator()
|
||||
{
|
||||
RuleFor(_ => _.Action).Equal(ClientAction.CreateGame);
|
||||
RuleFor(_ => _.GameName).NotEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using FluentValidation;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
||||
{
|
||||
public class ListGamesRequestValidator : AbstractValidator<ListGamesRequest>
|
||||
{
|
||||
public ListGamesRequestValidator()
|
||||
{
|
||||
RuleFor(_ => _.Action).Equal(ClientAction.ListGames);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using FluentValidation;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
||||
{
|
||||
public class LoadGameRequestValidator : AbstractValidator<LoadGameRequest>
|
||||
{
|
||||
public LoadGameRequestValidator()
|
||||
{
|
||||
RuleFor(_ => _.Action).Equal(ClientAction.LoadGame);
|
||||
RuleFor(_ => _.GameName).NotEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using FluentValidation;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
||||
{
|
||||
public class MoveRequestValidator : AbstractValidator<MoveRequest>
|
||||
{
|
||||
public MoveRequestValidator()
|
||||
{
|
||||
RuleFor(_ => _.Action).Equal(ClientAction.Move);
|
||||
RuleFor(_ => _.GameName).NotEmpty();
|
||||
RuleFor(_ => _.Move.From)
|
||||
.Null()
|
||||
.When(_ => _.Move.PieceFromCaptured.HasValue)
|
||||
.WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties.");
|
||||
RuleFor(_ => _.Move.From)
|
||||
.NotEmpty()
|
||||
.When(_ => !_.Move.PieceFromCaptured.HasValue)
|
||||
.WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,65 +31,44 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
||||
private readonly ILogger<SocketService> logger;
|
||||
private readonly ISocketConnectionManager communicationManager;
|
||||
private readonly IGameboardRepository gameboardRepository;
|
||||
private readonly ISocketTokenManager tokenManager;
|
||||
private readonly ICreateGameHandler createGameHandler;
|
||||
private readonly IGameboardManager gameboardManager;
|
||||
private readonly ISocketTokenCache tokenManager;
|
||||
private readonly IJoinByCodeHandler joinByCodeHandler;
|
||||
private readonly IJoinGameHandler joinGameHandler;
|
||||
private readonly IListGamesHandler listGamesHandler;
|
||||
private readonly ILoadGameHandler loadGameHandler;
|
||||
private readonly IMoveHandler moveHandler;
|
||||
private readonly IValidator<CreateGameRequest> createGameRequestValidator;
|
||||
private readonly IValidator<JoinByCodeRequest> joinByCodeRequestValidator;
|
||||
private readonly IValidator<JoinGameRequest> joinGameRequestValidator;
|
||||
private readonly IValidator<ListGamesRequest> listGamesRequestValidator;
|
||||
private readonly IValidator<LoadGameRequest> loadGameRequestValidator;
|
||||
private readonly IValidator<MoveRequest> moveRequestValidator;
|
||||
|
||||
public SocketService(
|
||||
ILogger<SocketService> logger,
|
||||
ISocketConnectionManager communicationManager,
|
||||
IGameboardRepository gameboardRepository,
|
||||
ISocketTokenManager tokenManager,
|
||||
ICreateGameHandler createGameHandler,
|
||||
IGameboardManager gameboardManager,
|
||||
ISocketTokenCache tokenManager,
|
||||
IJoinByCodeHandler joinByCodeHandler,
|
||||
IJoinGameHandler joinGameHandler,
|
||||
IListGamesHandler listGamesHandler,
|
||||
ILoadGameHandler loadGameHandler,
|
||||
IMoveHandler moveHandler,
|
||||
IValidator<CreateGameRequest> createGameRequestValidator,
|
||||
IValidator<JoinByCodeRequest> joinByCodeRequestValidator,
|
||||
IValidator<JoinGameRequest> joinGameRequestValidator,
|
||||
IValidator<ListGamesRequest> listGamesRequestValidator,
|
||||
IValidator<LoadGameRequest> loadGameRequestValidator,
|
||||
IValidator<MoveRequest> moveRequestValidator
|
||||
IValidator<JoinGameRequest> joinGameRequestValidator
|
||||
) : base()
|
||||
{
|
||||
this.logger = logger;
|
||||
this.communicationManager = communicationManager;
|
||||
this.gameboardRepository = gameboardRepository;
|
||||
this.gameboardManager = gameboardManager;
|
||||
this.tokenManager = tokenManager;
|
||||
this.createGameHandler = createGameHandler;
|
||||
this.joinByCodeHandler = joinByCodeHandler;
|
||||
this.joinGameHandler = joinGameHandler;
|
||||
this.listGamesHandler = listGamesHandler;
|
||||
this.loadGameHandler = loadGameHandler;
|
||||
this.moveHandler = moveHandler;
|
||||
this.createGameRequestValidator = createGameRequestValidator;
|
||||
this.joinByCodeRequestValidator = joinByCodeRequestValidator;
|
||||
this.joinGameRequestValidator = joinGameRequestValidator;
|
||||
this.listGamesRequestValidator = listGamesRequestValidator;
|
||||
this.loadGameRequestValidator = loadGameRequestValidator;
|
||||
this.moveRequestValidator = moveRequestValidator;
|
||||
}
|
||||
|
||||
public async Task HandleSocketRequest(HttpContext context)
|
||||
{
|
||||
string? userName = null;
|
||||
if (context.Request.Cookies.ContainsKey(SocketController.WebSessionKey))
|
||||
var user = await gameboardManager.ReadUser(context.User);
|
||||
if (user?.WebSessionId != null)
|
||||
{
|
||||
// Guest account
|
||||
var webSessionId = Guid.Parse(context.Request.Cookies[SocketController.WebSessionKey]!);
|
||||
userName = (await gameboardRepository.ReadGuestUser(webSessionId))?.Name;
|
||||
userName = tokenManager.GetUsername(user.WebSessionId.Value);
|
||||
}
|
||||
else if (context.Request.Query.Keys.Contains("token"))
|
||||
{
|
||||
@@ -123,24 +102,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
||||
}
|
||||
switch (request.Action)
|
||||
{
|
||||
case ClientAction.ListGames:
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<ListGamesRequest>(message);
|
||||
if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req))
|
||||
{
|
||||
await listGamesHandler.Handle(req, userName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClientAction.CreateGame:
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<CreateGameRequest>(message);
|
||||
if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req))
|
||||
{
|
||||
await createGameHandler.Handle(req, userName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClientAction.JoinGame:
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<JoinGameRequest>(message);
|
||||
@@ -159,24 +120,6 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClientAction.LoadGame:
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<LoadGameRequest>(message);
|
||||
if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req))
|
||||
{
|
||||
await loadGameHandler.Handle(req, userName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClientAction.Move:
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<MoveRequest>(message);
|
||||
if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req))
|
||||
{
|
||||
await moveHandler.Handle(req, userName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
|
||||
46
Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
Normal file
46
Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Standardizes the claims from third party issuers. Also registers new msal users in the database.
|
||||
/// </summary>
|
||||
public class ShogiUserClaimsTransformer : IClaimsTransformation
|
||||
{
|
||||
private static readonly string MsalUsernameClaim = "preferred_username";
|
||||
private readonly IGameboardRepository gameboardRepository;
|
||||
|
||||
public ShogiUserClaimsTransformer(IGameboardRepository gameboardRepository)
|
||||
{
|
||||
this.gameboardRepository = gameboardRepository;
|
||||
}
|
||||
|
||||
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
var nameClaim = principal.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim);
|
||||
if (nameClaim != default)
|
||||
{
|
||||
var user = await gameboardRepository.ReadUser(nameClaim.Value);
|
||||
if (user == null)
|
||||
{
|
||||
var newUser = new Models.User(nameClaim.Value);
|
||||
var success = await gameboardRepository.CreateUser(newUser);
|
||||
if (success) user = newUser;
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
return new ClaimsPrincipal(user.CreateMsalUserIdentity());
|
||||
}
|
||||
}
|
||||
return principal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,23 @@ using Gameboard.ShogiUI.Sockets.Repositories;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.Services;
|
||||
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Identity.Client;
|
||||
using Microsoft.Identity.Web;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -34,29 +40,16 @@ namespace Gameboard.ShogiUI.Sockets
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// Socket ActionHandlers
|
||||
services.AddSingleton<ICreateGameHandler, CreateGameHandler>();
|
||||
services.AddSingleton<IJoinByCodeHandler, JoinByCodeHandler>();
|
||||
services.AddSingleton<IJoinGameHandler, JoinGameHandler>();
|
||||
services.AddSingleton<IListGamesHandler, ListGamesHandler>();
|
||||
services.AddSingleton<ILoadGameHandler, LoadGameHandler>();
|
||||
services.AddSingleton<IMoveHandler, MoveHandler>();
|
||||
|
||||
// Managers
|
||||
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
||||
services.AddSingleton<ISocketTokenManager, SocketTokenManager>();
|
||||
services.AddSingleton<ISocketTokenCache, SocketTokenCache>();
|
||||
services.AddSingleton<IGameboardManager, GameboardManager>();
|
||||
|
||||
// Services
|
||||
services.AddSingleton<IValidator<CreateGameRequest>, CreateGameRequestValidator>();
|
||||
services.AddSingleton<IValidator<JoinByCodeRequest>, JoinByCodeRequestValidator>();
|
||||
services.AddSingleton<IValidator<JoinGameRequest>, JoinGameRequestValidator>();
|
||||
services.AddSingleton<IValidator<ListGamesRequest>, ListGamesRequestValidator>();
|
||||
services.AddSingleton<IValidator<LoadGameRequest>, LoadGameRequestValidator>();
|
||||
services.AddSingleton<IValidator<MoveRequest>, MoveRequestValidator>();
|
||||
services.AddSingleton<ISocketService, SocketService>();
|
||||
|
||||
// Repositories
|
||||
services.AddTransient<IGameboardRepository, GameboardRepository>();
|
||||
services.AddSingleton<IClaimsTransformation, ShogiUserClaimsTransformer>();
|
||||
services.AddHttpClient("couchdb", c =>
|
||||
{
|
||||
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin"));
|
||||
@@ -66,35 +59,54 @@ namespace Gameboard.ShogiUI.Sockets
|
||||
var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/";
|
||||
c.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
services.AddTransient<IGameboardRepository, GameboardRepository>();
|
||||
//services.AddSingleton<IAuthenticatedHttpClient, AuthenticatedHttpClient>();
|
||||
//services.AddSingleton<ICouchClient>(provider => new CouchClient(databaseName, couchUrl));
|
||||
|
||||
services.AddControllers();
|
||||
|
||||
services
|
||||
.AddAuthentication(options =>
|
||||
.AddControllers()
|
||||
.AddNewtonsoftJson(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
options.SerializerSettings.Formatting = Formatting.Indented;
|
||||
options.SerializerSettings.ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
options.Authority = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0";
|
||||
options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122";
|
||||
options.TokenValidationParameters.ValidateIssuer = true;
|
||||
options.TokenValidationParameters.ValidateAudience = true;
|
||||
NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true }
|
||||
};
|
||||
options.SerializerSettings.Converters = new[] { new StringEnumConverter() };
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
});
|
||||
|
||||
options.Events = new JwtBearerEvents
|
||||
services.AddAuthentication("CookieOrJwt")
|
||||
.AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options =>
|
||||
{
|
||||
OnMessageReceived = (context) =>
|
||||
options.ForwardDefaultSelector = context =>
|
||||
{
|
||||
if (context.HttpContext.WebSockets.IsWebSocketRequest)
|
||||
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
|
||||
return bearerAuth
|
||||
? JwtBearerDefaults.AuthenticationScheme
|
||||
: CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
};
|
||||
})
|
||||
.AddCookie(options =>
|
||||
{
|
||||
Console.WriteLine("Yep");
|
||||
}
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
options.Cookie.Name = "session-id";
|
||||
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
|
||||
options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
|
||||
options.SlidingExpiration = true;
|
||||
})
|
||||
.AddMicrosoftIdentityWebApi(Configuration);
|
||||
|
||||
services.AddSwaggerDocument(config =>
|
||||
{
|
||||
config.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme
|
||||
{
|
||||
Type = NSwag.OpenApiSecuritySchemeType.OAuth2,
|
||||
Flow = NSwag.OpenApiOAuth2Flow.AccessCode,
|
||||
AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||
Scopes = new Dictionary<string, string> { { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" } },
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
config.PostProcess = document =>
|
||||
{
|
||||
document.Info.Title = "Gameboard.ShogiUI.Sockets";
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -114,6 +126,18 @@ namespace Gameboard.ShogiUI.Sockets
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
var client = PublicClientApplicationBuilder
|
||||
.Create(Configuration["AzureAd:ClientId"])
|
||||
.WithLogging(
|
||||
(level, message, pii) =>
|
||||
{
|
||||
|
||||
},
|
||||
LogLevel.Verbose,
|
||||
true,
|
||||
true
|
||||
)
|
||||
.Build();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -121,17 +145,20 @@ namespace Gameboard.ShogiUI.Sockets
|
||||
}
|
||||
app
|
||||
.UseRequestResponseLogging()
|
||||
.UseCors(
|
||||
opt => opt
|
||||
.WithOrigins(origins)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.WithExposedHeaders("Set-Cookie")
|
||||
.AllowCredentials()
|
||||
)
|
||||
.UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials())
|
||||
.UseRouting()
|
||||
.UseAuthentication()
|
||||
.UseAuthorization()
|
||||
.UseOpenApi()
|
||||
.UseSwaggerUi3(config =>
|
||||
{
|
||||
config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings()
|
||||
{
|
||||
ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff",
|
||||
UsePkceWithAuthorizationCodeGrant = true
|
||||
};
|
||||
//config.WithCredentials = true;
|
||||
})
|
||||
.UseWebSockets(socketOptions)
|
||||
.UseEndpoints(endpoints =>
|
||||
{
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace Gameboard.ShogiUI.Sockets.Utilities
|
||||
{
|
||||
var file = (char)(x + A);
|
||||
var rank = y + 1;
|
||||
Console.WriteLine($"({x},{y}) - {file}{rank}");
|
||||
return $"{file}{rank}";
|
||||
}
|
||||
|
||||
|
||||
@@ -12,5 +12,11 @@
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AzureAd": {
|
||||
"Instance": "https://login.microsoftonline.com/",
|
||||
"ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff",
|
||||
"TenantId": "common",
|
||||
"Audience": "c1e94676-cab0-42ba-8b6c-9532b8486fff"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.17.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.17.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.2">
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
using AutoFixture;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Execution;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
|
||||
using Xunit;
|
||||
|
||||
namespace Gameboard.ShogiUI.xUnitTests.RequestValidators
|
||||
{
|
||||
public class MoveRequestValidatorShould
|
||||
{
|
||||
private readonly Fixture fixture;
|
||||
private readonly MoveRequestValidator validator;
|
||||
public MoveRequestValidatorShould()
|
||||
{
|
||||
fixture = new Fixture();
|
||||
validator = new MoveRequestValidator();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventInvalidPropertyCombinations()
|
||||
{
|
||||
// Arrange
|
||||
var request = fixture.Create<MoveRequest>();
|
||||
|
||||
// Act
|
||||
var results = validator.Validate(request);
|
||||
|
||||
// Assert
|
||||
using (new AssertionScope())
|
||||
{
|
||||
results.IsValid.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowValidPropertyCombinations()
|
||||
{
|
||||
// Arrange
|
||||
var requestWithoutFrom = new MoveRequest()
|
||||
{
|
||||
Action = ClientAction.Move,
|
||||
GameName = "Some game name",
|
||||
Move = new Move()
|
||||
{
|
||||
IsPromotion = false,
|
||||
PieceFromCaptured = WhichPiece.Bishop,
|
||||
To = "A4"
|
||||
}
|
||||
};
|
||||
var requestWithoutPieceFromCaptured = new MoveRequest()
|
||||
{
|
||||
Action = ClientAction.Move,
|
||||
GameName = "Some game name",
|
||||
Move = new Move()
|
||||
{
|
||||
From = "A1",
|
||||
IsPromotion = false,
|
||||
To = "A4"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = validator.Validate(requestWithoutFrom);
|
||||
var results2 = validator.Validate(requestWithoutPieceFromCaptured);
|
||||
|
||||
// Assert
|
||||
using (new AssertionScope())
|
||||
{
|
||||
results.IsValid.Should().BeTrue();
|
||||
results2.IsValid.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,6 @@ namespace PathFinding
|
||||
var element = collection[from];
|
||||
if (element == null)
|
||||
{
|
||||
Console.WriteLine("Null element in PathEvery");
|
||||
return;
|
||||
}
|
||||
foreach (var path in element.MoveSet.GetMoves())
|
||||
|
||||
Reference in New Issue
Block a user