create, read, playercount
This commit is contained in:
@@ -1,32 +1,32 @@
|
|||||||
using Shogi.Api.Managers;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Shogi.Api.Extensions;
|
||||||
|
using Shogi.Api.Managers;
|
||||||
using Shogi.Api.Repositories;
|
using Shogi.Api.Repositories;
|
||||||
using Shogi.Contracts.Api;
|
using Shogi.Contracts.Api;
|
||||||
using Shogi.Contracts.Socket;
|
using Shogi.Contracts.Socket;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Data.SqlClient;
|
|
||||||
using Shogi.Contracts.Types;
|
using Shogi.Contracts.Types;
|
||||||
using Shogi.Api.Extensions;
|
using System.Data.SqlClient;
|
||||||
|
|
||||||
namespace Shogi.Api.Controllers;
|
namespace Shogi.Api.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class SessionController : ControllerBase
|
public class SessionsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ISocketConnectionManager communicationManager;
|
private readonly ISocketConnectionManager communicationManager;
|
||||||
private readonly IModelMapper mapper;
|
private readonly IModelMapper mapper;
|
||||||
private readonly ISessionRepository sessionRepository;
|
private readonly ISessionRepository sessionRepository;
|
||||||
private readonly IQueryRespository queryRespository;
|
private readonly IQueryRespository queryRespository;
|
||||||
private readonly ILogger<SessionController> logger;
|
private readonly ILogger<SessionsController> logger;
|
||||||
|
|
||||||
public SessionController(
|
public SessionsController(
|
||||||
ISocketConnectionManager communicationManager,
|
ISocketConnectionManager communicationManager,
|
||||||
IModelMapper mapper,
|
IModelMapper mapper,
|
||||||
ISessionRepository sessionRepository,
|
ISessionRepository sessionRepository,
|
||||||
IQueryRespository queryRespository,
|
IQueryRespository queryRespository,
|
||||||
ILogger<SessionController> logger)
|
ILogger<SessionsController> logger)
|
||||||
{
|
{
|
||||||
this.communicationManager = communicationManager;
|
this.communicationManager = communicationManager;
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
@@ -39,14 +39,10 @@ public class SessionController : ControllerBase
|
|||||||
public async Task<IActionResult> CreateSession([FromBody] CreateSessionCommand request)
|
public async Task<IActionResult> CreateSession([FromBody] CreateSessionCommand request)
|
||||||
{
|
{
|
||||||
var userId = User.GetShogiUserId();
|
var userId = User.GetShogiUserId();
|
||||||
if (string.IsNullOrWhiteSpace(userId)) return this.Unauthorized();
|
var session = new Domain.Aggregates.Session(request.Name, userId);
|
||||||
var session = new Domain.Aggregates.Session(
|
|
||||||
request.Name,
|
|
||||||
userId,
|
|
||||||
new Domain.ValueObjects.ShogiBoard(Domain.BoardState.StandardStarting));
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await sessionRepository.CreateShogiBoard(board, request.Name, userId);
|
await sessionRepository.CreateSession(session);
|
||||||
}
|
}
|
||||||
catch (SqlException e)
|
catch (SqlException e)
|
||||||
{
|
{
|
||||||
@@ -58,6 +54,23 @@ public class SessionController : ControllerBase
|
|||||||
return CreatedAtAction(nameof(CreateSession), new { sessionName = request.Name }, null);
|
return CreatedAtAction(nameof(CreateSession), new { sessionName = request.Name }, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{name}")]
|
||||||
|
public async Task<IActionResult> DeleteSession(string name)
|
||||||
|
{
|
||||||
|
var userId = User.GetShogiUserId();
|
||||||
|
var session = await sessionRepository.ReadSession(name);
|
||||||
|
|
||||||
|
if (session == null) return this.NoContent();
|
||||||
|
|
||||||
|
if (session.Player1 == userId)
|
||||||
|
{
|
||||||
|
await sessionRepository.DeleteSession(name);
|
||||||
|
return this.NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.Unauthorized("Cannot delete sessions created by others.");
|
||||||
|
}
|
||||||
|
|
||||||
//[HttpPost("{sessionName}/Move")]
|
//[HttpPost("{sessionName}/Move")]
|
||||||
//public async Task<IActionResult> MovePiece([FromRoute] string sessionName, [FromBody] MovePieceCommand request)
|
//public async Task<IActionResult> MovePiece([FromRoute] string sessionName, [FromBody] MovePieceCommand request)
|
||||||
//{
|
//{
|
||||||
@@ -156,12 +169,12 @@ public class SessionController : ControllerBase
|
|||||||
// return Ok(response);
|
// return Ok(response);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet("PlayerCount")]
|
||||||
public async Task<ActionResult<ReadAllSessionsResponse>> GetSessions()
|
public async Task<ActionResult<ReadSessionsPlayerCountResponse>> GetSessionsPlayerCount()
|
||||||
{
|
{
|
||||||
var sessions = await this.queryRespository.ReadAllSessionsMetadata();
|
var sessions = await this.queryRespository.ReadSessionPlayerCount();
|
||||||
|
|
||||||
return Ok(new ReadAllSessionsResponse
|
return Ok(new ReadSessionsPlayerCountResponse
|
||||||
{
|
{
|
||||||
PlayerHasJoinedSessions = Array.Empty<SessionMetadata>(),
|
PlayerHasJoinedSessions = Array.Empty<SessionMetadata>(),
|
||||||
AllOtherSessions = sessions.ToList()
|
AllOtherSessions = sessions.ToList()
|
||||||
@@ -171,8 +184,26 @@ public class SessionController : ControllerBase
|
|||||||
[HttpGet("{name}")]
|
[HttpGet("{name}")]
|
||||||
public async Task<ActionResult<ReadSessionResponse>> GetSession(string name)
|
public async Task<ActionResult<ReadSessionResponse>> GetSession(string name)
|
||||||
{
|
{
|
||||||
await Task.CompletedTask;
|
var session = await sessionRepository.ReadSession(name);
|
||||||
return new ReadSessionResponse();
|
if (session == null) return this.NotFound();
|
||||||
|
|
||||||
|
return new ReadSessionResponse
|
||||||
|
{
|
||||||
|
Session = new Session
|
||||||
|
{
|
||||||
|
BoardState = new BoardState
|
||||||
|
{
|
||||||
|
Board = session.Board.BoardState.State.ToContract(),
|
||||||
|
Player1Hand = session.Board.BoardState.Player1Hand.ToContract(),
|
||||||
|
Player2Hand = session.Board.BoardState.Player2Hand.ToContract(),
|
||||||
|
PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(),
|
||||||
|
WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract()
|
||||||
|
},
|
||||||
|
Player1 = session.Player1,
|
||||||
|
Player2 = session.Player2,
|
||||||
|
SessionName = session.Name
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//[HttpPut("{sessionName}")]
|
//[HttpPut("{sessionName}")]
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Shogi.Contracts.Api;
|
|
||||||
using Shogi.Api.Extensions;
|
using Shogi.Api.Extensions;
|
||||||
using Shogi.Api.Managers;
|
using Shogi.Api.Managers;
|
||||||
using Shogi.Api.Models;
|
|
||||||
using Shogi.Api.Repositories;
|
using Shogi.Api.Repositories;
|
||||||
using System.Security.Claims;
|
using Shogi.Contracts.Api;
|
||||||
|
|
||||||
namespace Shogi.Api.Controllers;
|
namespace Shogi.Api.Controllers;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Shogi.Api.Extensions;
|
namespace Shogi.Api.Extensions;
|
||||||
|
|
||||||
public static class Extensions
|
public static class ClaimsExtensions
|
||||||
{
|
{
|
||||||
private static readonly string MsalUsernameClaim = "preferred_username";
|
private static readonly string MsalUsernameClaim = "preferred_username";
|
||||||
|
|
||||||
@@ -26,5 +26,17 @@ public static class Extensions
|
|||||||
return self.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim)?.Value;
|
return self.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim)?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string? GetShogiUserId(this ClaimsPrincipal self) => self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId();
|
/// <summary>
|
||||||
|
/// Reads the userId from claims after claims transformation has occurred.
|
||||||
|
/// Throws if a shogi userid is not found.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public static string GetShogiUserId(this ClaimsPrincipal self)
|
||||||
|
{
|
||||||
|
var id = self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(id)) throw new InvalidOperationException("Shogi UserId not found in claims.");
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
52
Shogi.Api/Extensions/ContractsExtensions.cs
Normal file
52
Shogi.Api/Extensions/ContractsExtensions.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using Shogi.Contracts.Types;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Shogi.Api.Extensions;
|
||||||
|
|
||||||
|
public static class ContractsExtensions
|
||||||
|
{
|
||||||
|
public static WhichPlayer ToContract(this Domain.WhichPlayer player)
|
||||||
|
{
|
||||||
|
return player switch
|
||||||
|
{
|
||||||
|
Domain.WhichPlayer.Player1 => WhichPlayer.Player1,
|
||||||
|
Domain.WhichPlayer.Player2 => WhichPlayer.Player2,
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WhichPiece ToContract(this Domain.WhichPiece piece)
|
||||||
|
{
|
||||||
|
return piece switch
|
||||||
|
{
|
||||||
|
Domain.WhichPiece.King => WhichPiece.King,
|
||||||
|
Domain.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral,
|
||||||
|
Domain.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral,
|
||||||
|
Domain.WhichPiece.Bishop => WhichPiece.Bishop,
|
||||||
|
Domain.WhichPiece.Rook => WhichPiece.Rook,
|
||||||
|
Domain.WhichPiece.Knight => WhichPiece.Knight,
|
||||||
|
Domain.WhichPiece.Lance => WhichPiece.Lance,
|
||||||
|
Domain.WhichPiece.Pawn => WhichPiece.Pawn,
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Piece ToContract(this Domain.ValueObjects.Piece piece) => new()
|
||||||
|
{
|
||||||
|
IsPromoted = piece.IsPromoted,
|
||||||
|
Owner = piece.Owner.ToContract(),
|
||||||
|
WhichPiece = piece.WhichPiece.ToContract()
|
||||||
|
};
|
||||||
|
|
||||||
|
public static IReadOnlyList<Piece> ToContract(this List<Domain.ValueObjects.Piece> pieces)
|
||||||
|
{
|
||||||
|
return pieces
|
||||||
|
.Select(ToContract)
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, Piece?> ToContract(this ReadOnlyDictionary<string, Domain.ValueObjects.Piece?> boardState) =>
|
||||||
|
boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract());
|
||||||
|
}
|
||||||
@@ -2,12 +2,11 @@ using Microsoft.AspNetCore.Authentication;
|
|||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Http.Json;
|
||||||
using Microsoft.AspNetCore.HttpLogging;
|
using Microsoft.AspNetCore.HttpLogging;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Identity.Web;
|
using Microsoft.Identity.Web;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Converters;
|
|
||||||
using Newtonsoft.Json.Serialization;
|
|
||||||
using Shogi.Api.Managers;
|
using Shogi.Api.Managers;
|
||||||
using Shogi.Api.Repositories;
|
using Shogi.Api.Repositories;
|
||||||
using Shogi.Api.Services;
|
using Shogi.Api.Services;
|
||||||
@@ -38,7 +37,7 @@ namespace Shogi.Api
|
|||||||
});
|
});
|
||||||
|
|
||||||
ConfigureAuthentication(builder);
|
ConfigureAuthentication(builder);
|
||||||
ConfigureControllersWithNewtonsoft(builder);
|
ConfigureControllers(builder);
|
||||||
ConfigureSwagger(builder);
|
ConfigureSwagger(builder);
|
||||||
ConfigureDependencyInjection(builder);
|
ConfigureDependencyInjection(builder);
|
||||||
ConfigureLogging(builder);
|
ConfigureLogging(builder);
|
||||||
@@ -165,39 +164,13 @@ namespace Shogi.Api
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureControllersWithNewtonsoft(WebApplicationBuilder builder)
|
private static void ConfigureControllers(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Services
|
builder.Services.AddControllers();
|
||||||
.AddControllers()
|
builder.Services.Configure<JsonOptions>(options =>
|
||||||
//.AddJsonOptions(options =>
|
|
||||||
//{
|
|
||||||
// options.AllowInputFormatterExceptionMessages = true;
|
|
||||||
// options.JsonSerializerOptions.WriteIndented = true;
|
|
||||||
//});
|
|
||||||
.AddNewtonsoftJson(options =>
|
|
||||||
{
|
{
|
||||||
options.SerializerSettings.Formatting = Formatting.Indented;
|
options.SerializerOptions.WriteIndented = true;
|
||||||
options.SerializerSettings.ContractResolver = new DefaultContractResolver
|
|
||||||
{
|
|
||||||
NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = false }
|
|
||||||
};
|
|
||||||
options.SerializerSettings.Converters = new[] { new StringEnumConverter() };
|
|
||||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
Formatting = Formatting.Indented,
|
|
||||||
ContractResolver = new DefaultContractResolver
|
|
||||||
{
|
|
||||||
NamingStrategy = new CamelCaseNamingStrategy
|
|
||||||
{
|
|
||||||
ProcessDictionaryKeys = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Converters = new[] { new StringEnumConverter() },
|
|
||||||
NullValueHandling = NullValueHandling.Ignore,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureDependencyInjection(WebApplicationBuilder builder)
|
private static void ConfigureDependencyInjection(WebApplicationBuilder builder)
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
using Shogi.Domain;
|
|
||||||
using Shogi.Domain.ValueObjects;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Api.Repositories.Dto;
|
|
||||||
|
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
public class BoardStateDto
|
|
||||||
{
|
|
||||||
public ReadOnlyDictionary<string, Piece?> State { get; set; }
|
|
||||||
|
|
||||||
public List<Piece> Player1Hand { get; set; }
|
|
||||||
|
|
||||||
public List<Piece> Player2Hand { get; set; }
|
|
||||||
|
|
||||||
public Move PreviousMove { get; }
|
|
||||||
|
|
||||||
public WhichPlayer WhoseTurn { get; set; }
|
|
||||||
public WhichPlayer? InCheck { get; set; }
|
|
||||||
public bool IsCheckmate { get; set; }
|
|
||||||
}
|
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
10
Shogi.Api/Repositories/Dto/MoveDto.cs
Normal file
10
Shogi.Api/Repositories/Dto/MoveDto.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Shogi.Domain;
|
||||||
|
|
||||||
|
namespace Shogi.Api.Repositories.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Useful with Dapper to read from database.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct MoveDto(string From, string To, bool IsPromotion, WhichPiece? PieceFromHand)
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,14 +1,5 @@
|
|||||||
namespace Shogi.Api.Repositories.Dto;
|
namespace Shogi.Api.Repositories.Dto;
|
||||||
|
|
||||||
/// <summary>
|
public readonly record struct SessionDto(string Name, string Player1, string Player2)
|
||||||
/// Useful with Dapper to read from database.
|
|
||||||
/// </summary>
|
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
public class SessionDto
|
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
|
||||||
public string Player1 { get; set; }
|
|
||||||
public string Player2 { get; set; }
|
|
||||||
public BoardStateDto BoardState { get; set; }
|
|
||||||
}
|
}
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using Shogi.Api.Repositories.Dto;
|
|
||||||
using Shogi.Contracts.Types;
|
using Shogi.Contracts.Types;
|
||||||
using System.Data.SqlClient;
|
using System.Data.SqlClient;
|
||||||
|
|
||||||
@@ -14,16 +13,16 @@ public class QueryRepository : IQueryRespository
|
|||||||
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SessionMetadata>> ReadAllSessionsMetadata()
|
public async Task<IEnumerable<SessionMetadata>> ReadSessionPlayerCount()
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
return await connection.QueryAsync<SessionMetadata>(
|
return await connection.QueryAsync<SessionMetadata>(
|
||||||
"session.ReadAllSessionsMetadata",
|
"session.ReadSessionPlayerCount",
|
||||||
commandType: System.Data.CommandType.StoredProcedure);
|
commandType: System.Data.CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IQueryRespository
|
public interface IQueryRespository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<SessionMetadata>> ReadAllSessionsMetadata();
|
Task<IEnumerable<SessionMetadata>> ReadSessionPlayerCount();
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using Shogi.Api.Repositories.Dto;
|
using Shogi.Api.Repositories.Dto;
|
||||||
using Shogi.Domain;
|
|
||||||
using Shogi.Domain.Aggregates;
|
using Shogi.Domain.Aggregates;
|
||||||
using Shogi.Domain.ValueObjects;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Data.SqlClient;
|
using System.Data.SqlClient;
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Shogi.Api.Repositories;
|
namespace Shogi.Api.Repositories;
|
||||||
|
|
||||||
@@ -20,45 +17,52 @@ public class SessionRepository : ISessionRepository
|
|||||||
|
|
||||||
public async Task CreateSession(Session session)
|
public async Task CreateSession(Session session)
|
||||||
{
|
{
|
||||||
var boardStateDto = new BoardStateDto
|
|
||||||
{
|
|
||||||
InCheck = session.BoardState.InCheck,
|
|
||||||
IsCheckmate = session.BoardState.IsCheckmate,
|
|
||||||
Player1Hand = session.BoardState.Player1Hand,
|
|
||||||
Player2Hand = session.BoardState.Player2Hand,
|
|
||||||
State = session.BoardState.State,
|
|
||||||
WhoseTurn = session.BoardState.WhoseTurn,
|
|
||||||
};
|
|
||||||
|
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"session.CreateSession",
|
"session.CreateSession",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Name = sessionName,
|
session.Name,
|
||||||
InitialBoardStateDocument = JsonSerializer.Serialize(boardStateDto),
|
Player1Name = session.Player1,
|
||||||
Player1Name = player1,
|
|
||||||
},
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ShogiBoard?> ReadShogiBoard(string name)
|
public async Task DeleteSession(string name)
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
var results = await connection.QueryAsync<SessionDto>(
|
await connection.ExecuteAsync(
|
||||||
"session.ReadSession",
|
"session.DeleteSession",
|
||||||
|
new { Name = name },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
var dto = results.SingleOrDefault();
|
}
|
||||||
if (dto == null) return null;
|
|
||||||
|
|
||||||
var boardState = new BoardState(
|
public async Task<Session?> ReadSession(string name)
|
||||||
state: new(dto.BoardState.State),
|
{
|
||||||
player1Hand: dto.BoardState.Player1Hand,
|
using var connection = new SqlConnection(connectionString);
|
||||||
player2Hand: dto.BoardState.Player2Hand,
|
var results = await connection.QueryMultipleAsync(
|
||||||
whoseTurn: dto.BoardState.WhoseTurn,
|
"session.ReadSession",
|
||||||
playerInCheck: dto.BoardState.InCheck,
|
new { Name = name },
|
||||||
previousMove: dto.BoardState.PreviousMove);
|
commandType: CommandType.StoredProcedure);
|
||||||
var session = new ShogiBoard(boardState);
|
|
||||||
|
var sessionDtos = await results.ReadAsync<SessionDto>();
|
||||||
|
if (!sessionDtos.Any()) return null;
|
||||||
|
var dto = sessionDtos.First();
|
||||||
|
var session = new Session(dto.Name, dto.Player1);
|
||||||
|
if (!string.IsNullOrWhiteSpace(dto.Player2)) session.AddPlayer2(dto.Player2);
|
||||||
|
|
||||||
|
var moveDtos = await results.ReadAsync<MoveDto>();
|
||||||
|
foreach (var move in moveDtos)
|
||||||
|
{
|
||||||
|
if (move.PieceFromHand.HasValue)
|
||||||
|
{
|
||||||
|
session.Board.Move(move.PieceFromHand.Value, move.To);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
session.Board.Move(move.From, move.To, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,5 +70,6 @@ public class SessionRepository : ISessionRepository
|
|||||||
public interface ISessionRepository
|
public interface ISessionRepository
|
||||||
{
|
{
|
||||||
Task CreateSession(Session session);
|
Task CreateSession(Session session);
|
||||||
Task<ShogiBoard?> ReadShogiBoard(string name);
|
Task DeleteSession(string name);
|
||||||
|
Task<Session?> ReadSession(string name);
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Shogi.Contracts.Socket;
|
using Shogi.Contracts.Socket;
|
||||||
using Shogi.Contracts.Types;
|
using Shogi.Contracts.Types;
|
||||||
using Shogi.Api.Extensions;
|
using Shogi.Api.Extensions;
|
||||||
using Shogi.Api.Managers;
|
using Shogi.Api.Managers;
|
||||||
using Shogi.Api.Repositories;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Shogi.Api.Services
|
namespace Shogi.Api.Services
|
||||||
{
|
{
|
||||||
@@ -59,7 +58,7 @@ namespace Shogi.Api.Services
|
|||||||
var message = await socket.ReceiveTextAsync();
|
var message = await socket.ReceiveTextAsync();
|
||||||
if (string.IsNullOrWhiteSpace(message)) continue;
|
if (string.IsNullOrWhiteSpace(message)) continue;
|
||||||
logger.LogInformation("Request \n{0}\n", message);
|
logger.LogInformation("Request \n{0}\n", message);
|
||||||
var request = JsonConvert.DeserializeObject<ISocketRequest>(message);
|
var request = JsonSerializer.Deserialize<ISocketRequest>(message);
|
||||||
if (request == null || !Enum.IsDefined(typeof(SocketAction), request.Action))
|
if (request == null || !Enum.IsDefined(typeof(SocketAction), request.Action))
|
||||||
{
|
{
|
||||||
await socket.SendTextAsync("Error: Action not recognized.");
|
await socket.SendTextAsync("Error: Action not recognized.");
|
||||||
|
|||||||
@@ -29,12 +29,10 @@
|
|||||||
<PackageReference Include="FluentValidation" Version="11.2.0" />
|
<PackageReference Include="FluentValidation" Version="11.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.2" />
|
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.2" />
|
||||||
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.25.2" />
|
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.25.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.4" />
|
<PackageReference Include="System.Data.SqlClient" Version="4.8.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ namespace Shogi.Contracts.Api;
|
|||||||
public class CreateSessionCommand
|
public class CreateSessionCommand
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; }
|
||||||
public bool IsPrivate { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Shogi.Contracts.Api;
|
namespace Shogi.Contracts.Api;
|
||||||
|
|
||||||
public class ReadAllSessionsResponse
|
public class ReadSessionsPlayerCountResponse
|
||||||
{
|
{
|
||||||
public IList<SessionMetadata> PlayerHasJoinedSessions { get; set; }
|
public IList<SessionMetadata> PlayerHasJoinedSessions { get; set; }
|
||||||
public IList<SessionMetadata> AllOtherSessions { get; set; }
|
public IList<SessionMetadata> AllOtherSessions { get; set; }
|
||||||
|
|||||||
11
Shogi.Contracts/ShogiApiJsonSerializerSettings.cs
Normal file
11
Shogi.Contracts/ShogiApiJsonSerializerSettings.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts;
|
||||||
|
|
||||||
|
public class ShogiApiJsonSerializerSettings
|
||||||
|
{
|
||||||
|
public readonly static JsonSerializerOptions SystemTextJsonSerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
public class Session
|
public class Session
|
||||||
{
|
{
|
||||||
public string Player1 { get; set; } = string.Empty;
|
public string Player1 { get; set; }
|
||||||
public string? Player2 { get; set; }
|
public string? Player2 { get; set; }
|
||||||
public string SessionName { get; set; } = string.Empty;
|
public string SessionName { get; set; }
|
||||||
public bool GameOver { get; set; }
|
|
||||||
public BoardState BoardState { get; set; }
|
public BoardState BoardState { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ Post-Deployment Script Template
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:r .\Scripts\PopulateLoginPlatforms.sql
|
:r .\Scripts\PopulateLoginPlatforms.sql
|
||||||
|
:r .\Scripts\PopulatePieces.sql
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER DATABASE Shogi
|
||||||
|
SET ALLOW_SNAPSHOT_ISOLATION ON
|
||||||
21
Shogi.Database/Post Deployment/Scripts/PopulatePieces.sql
Normal file
21
Shogi.Database/Post Deployment/Scripts/PopulatePieces.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
DECLARE @Pieces TABLE(
|
||||||
|
[Name] NVARCHAR(13)
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO @Pieces ([Name])
|
||||||
|
VALUES
|
||||||
|
('King'),
|
||||||
|
('GoldGeneral'),
|
||||||
|
('SilverGeneral'),
|
||||||
|
('Bishop'),
|
||||||
|
('Rook'),
|
||||||
|
('Knight'),
|
||||||
|
('Lance'),
|
||||||
|
('Pawn');
|
||||||
|
|
||||||
|
MERGE [session].[Piece] as t
|
||||||
|
USING @Pieces as s
|
||||||
|
ON t.[Name] = s.[Name]
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT ([Name])
|
||||||
|
VALUES (s.[Name]);
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
CREATE PROCEDURE [session].[CreateSession]
|
CREATE PROCEDURE [session].[CreateSession]
|
||||||
@Name [session].[SessionName],
|
@Name [session].[SessionName],
|
||||||
@Player1Name [user].[UserName],
|
@Player1Name [user].[UserName]
|
||||||
@InitialBoardStateDocument [session].[JsonDocument]
|
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
|
|
||||||
INSERT INTO [session].[Session] (BoardState, Player1Id)
|
INSERT INTO [session].[Session] ([Name], Player1Id)
|
||||||
SELECT
|
SELECT @Name, Id
|
||||||
@InitialBoardStateDocument,
|
|
||||||
Id
|
|
||||||
FROM [user].[User]
|
FROM [user].[User]
|
||||||
WHERE [Name] = @Player1Name
|
WHERE [Name] = @Player1Name
|
||||||
END
|
END
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE PROCEDURE [session].[DeleteSession]
|
||||||
|
@Name [session].[SessionName]
|
||||||
|
AS
|
||||||
|
|
||||||
|
DELETE FROM [session].[Session] WHERE [Name] = @Name;
|
||||||
@@ -2,15 +2,32 @@
|
|||||||
@Name [session].[SessionName]
|
@Name [session].[SessionName]
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON -- Performance boost
|
||||||
|
SET XACT_ABORT ON -- Rollback transaction on error
|
||||||
|
SET TRANSACTION ISOLATION LEVEL SNAPSHOT -- Ignores data changes that happen after the transaction begins.
|
||||||
|
|
||||||
|
BEGIN TRANSACTION
|
||||||
|
|
||||||
|
-- Session
|
||||||
SELECT
|
SELECT
|
||||||
sess.[Name],
|
sess.[Name],
|
||||||
BoardState,
|
|
||||||
p1.[Name] as Player1,
|
p1.[Name] as Player1,
|
||||||
p2.[Name] as Player2
|
p2.[Name] as Player2
|
||||||
FROM [session].[Session] sess
|
FROM [session].[Session] sess
|
||||||
INNER JOIN [user].[User] p1 on sess.Player1Id = p1.Id
|
INNER JOIN [user].[User] p1 on sess.Player1Id = p1.Id
|
||||||
LEFT JOIN [user].[User] p2 on sess.Player2Id = p2.Id
|
LEFT JOIN [user].[User] p2 on sess.Player2Id = p2.Id
|
||||||
WHERE sess.[Name] = @Name;
|
WHERE sess.[Name] = @Name;
|
||||||
|
|
||||||
|
-- Player moves
|
||||||
|
SELECT
|
||||||
|
mv.[From],
|
||||||
|
mv.[To],
|
||||||
|
mv.IsPromotion,
|
||||||
|
piece.[Name] as PieceFromHand
|
||||||
|
FROM [session].[Move] mv
|
||||||
|
INNER JOIN [session].[Session] sess ON sess.Id = mv.SessionId
|
||||||
|
RIGHT JOIN [session].Piece piece on piece.Id = mv.PieceIdFromHand
|
||||||
|
WHERE sess.[Name] = @Name;
|
||||||
|
|
||||||
|
COMMIT
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
CREATE PROCEDURE [session].[ReadAllSessionsMetadata]
|
CREATE PROCEDURE [session].[ReadSessionPlayerCount]
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON;
|
SET NOCOUNT ON;
|
||||||
20
Shogi.Database/Session/Tables/Move.sql
Normal file
20
Shogi.Database/Session/Tables/Move.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE [session].[Move]
|
||||||
|
(
|
||||||
|
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
|
||||||
|
[SessionId] BIGINT NOT NULL,
|
||||||
|
[From] VARCHAR(2) NOT NULL,
|
||||||
|
[To] VARCHAR(2) NOT NULL,
|
||||||
|
[IsPromotion] BIT NOT NULL,
|
||||||
|
[PieceIdFromHand] INT NULL
|
||||||
|
|
||||||
|
CONSTRAINT [Cannot end where you start]
|
||||||
|
CHECK ([From] <> [To]),
|
||||||
|
|
||||||
|
CONSTRAINT FK_Move_Session FOREIGN KEY (SessionId) REFERENCES [session].[Session] (Id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT FK_Move_Piece FOREIGN KEY (PieceIdFromHand) REFERENCES [session].[Piece] (Id)
|
||||||
|
ON DELETE NO ACTION
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
)
|
||||||
5
Shogi.Database/Session/Tables/Piece.sql
Normal file
5
Shogi.Database/Session/Tables/Piece.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE [session].[Piece]
|
||||||
|
(
|
||||||
|
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
|
||||||
|
[Name] NVARCHAR(13) NOT NULL UNIQUE
|
||||||
|
)
|
||||||
@@ -4,13 +4,12 @@
|
|||||||
[Name] [session].[SessionName] UNIQUE,
|
[Name] [session].[SessionName] UNIQUE,
|
||||||
Player1Id BIGINT NOT NULL,
|
Player1Id BIGINT NOT NULL,
|
||||||
Player2Id BIGINT NULL,
|
Player2Id BIGINT NULL,
|
||||||
BoardState [session].[JsonDocument] NOT NULL,
|
|
||||||
Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(),
|
Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(),
|
||||||
|
|
||||||
CONSTRAINT [BoardState must be json] CHECK (isjson(BoardState)=1),
|
|
||||||
CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id)
|
CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
ON UPDATE CASCADE,
|
ON UPDATE CASCADE,
|
||||||
|
|
||||||
CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id)
|
CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id)
|
||||||
ON DELETE NO ACTION
|
ON DELETE NO ACTION
|
||||||
ON UPDATE NO ACTION
|
ON UPDATE NO ACTION
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<SqlServerVerification>False</SqlServerVerification>
|
<SqlServerVerification>False</SqlServerVerification>
|
||||||
<IncludeCompositeObjects>True</IncludeCompositeObjects>
|
<IncludeCompositeObjects>True</IncludeCompositeObjects>
|
||||||
<TargetDatabaseSet>True</TargetDatabaseSet>
|
<TargetDatabaseSet>True</TargetDatabaseSet>
|
||||||
|
<DefaultSchema>session</DefaultSchema>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
@@ -77,14 +78,21 @@
|
|||||||
<Build Include="User\Types\UserName.sql" />
|
<Build Include="User\Types\UserName.sql" />
|
||||||
<Build Include="Session\Types\JsonDocument.sql" />
|
<Build Include="Session\Types\JsonDocument.sql" />
|
||||||
<Build Include="User\StoredProcedures\CreateUser.sql" />
|
<Build Include="User\StoredProcedures\CreateUser.sql" />
|
||||||
<Build Include="Session\Stored Procedures\ReadAllSessionsMetadata.sql" />
|
<Build Include="Session\Stored Procedures\ReadSessionPlayerCount.sql" />
|
||||||
<Build Include="User\StoredProcedures\ReadUser.sql" />
|
<Build Include="User\StoredProcedures\ReadUser.sql" />
|
||||||
<Build Include="User\Tables\LoginPlatform.sql" />
|
<Build Include="User\Tables\LoginPlatform.sql" />
|
||||||
<None Include="Post Deployment\Scripts\PopulateLoginPlatforms.sql" />
|
<None Include="Post Deployment\Scripts\PopulateLoginPlatforms.sql" />
|
||||||
<Build Include="Session\Stored Procedures\UpdateSession.sql" />
|
<Build Include="Session\Stored Procedures\UpdateSession.sql" />
|
||||||
<Build Include="Session\Stored Procedures\ReadSession.sql" />
|
<Build Include="Session\Stored Procedures\ReadSession.sql" />
|
||||||
|
<Build Include="Session\Tables\Move.sql" />
|
||||||
|
<Build Include="Session\Tables\Piece.sql" />
|
||||||
|
<Build Include="Session\Stored Procedures\DeleteSession.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PostDeploy Include="Post Deployment\Script.PostDeployment.sql" />
|
<PostDeploy Include="Post Deployment\Script.PostDeployment.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Post Deployment\Scripts\PopulatePieces.sql" />
|
||||||
|
<None Include="Post Deployment\Scripts\EnableSnapshotIsolationLevel.sql" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -6,12 +6,11 @@ public class Session
|
|||||||
{
|
{
|
||||||
public Session(
|
public Session(
|
||||||
string name,
|
string name,
|
||||||
string player1Name,
|
string player1Name)
|
||||||
ShogiBoard board)
|
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Player1 = player1Name;
|
Player1 = player1Name;
|
||||||
Board = board;
|
Board = new(BoardState.StandardStarting);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public class BoardState
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Board state before any moves have been made, using standard setup and rules.
|
/// Board state before any moves have been made, using standard setup and rules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly BoardState StandardStarting = new(
|
public static BoardState StandardStarting => new(
|
||||||
state: BuildStandardStartingBoardState(),
|
state: BuildStandardStartingBoardState(),
|
||||||
player1Hand: new(),
|
player1Hand: new(),
|
||||||
player2Hand: new(),
|
player2Hand: new(),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Shogi.UI.Pages.Home.Api
|
|||||||
{
|
{
|
||||||
Task<CreateGuestTokenResponse?> GetGuestToken();
|
Task<CreateGuestTokenResponse?> GetGuestToken();
|
||||||
Task<Session?> GetSession(string name);
|
Task<Session?> GetSession(string name);
|
||||||
Task<ReadAllSessionsResponse?> GetSessions();
|
Task<ReadSessionsPlayerCountResponse?> GetSessions();
|
||||||
Task<Guid?> GetToken();
|
Task<Guid?> GetToken();
|
||||||
Task GuestLogout();
|
Task GuestLogout();
|
||||||
Task PostMove(string sessionName, Move move);
|
Task PostMove(string sessionName, Move move);
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ namespace Shogi.UI.Pages.Home.Api
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ReadAllSessionsResponse?> GetSessions()
|
public async Task<ReadSessionsPlayerCountResponse?> GetSessions()
|
||||||
{
|
{
|
||||||
var response = await HttpClient.GetAsync(new Uri("Session", UriKind.Relative));
|
var response = await HttpClient.GetAsync(new Uri("Session", UriKind.Relative));
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return await response.Content.ReadFromJsonAsync<ReadAllSessionsResponse>(serializerOptions);
|
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,6 @@ namespace Shogi.UI.Pages.Home.Api
|
|||||||
var response = await HttpClient.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand
|
var response = await HttpClient.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
IsPrivate = isPrivate
|
|
||||||
});
|
});
|
||||||
return response.StatusCode;
|
return response.StatusCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Shogi.AcceptanceTests.TestSetup;
|
using Shogi.AcceptanceTests.TestSetup;
|
||||||
using Shogi.Contracts.Api;
|
using Shogi.Contracts.Api;
|
||||||
|
using Shogi.Contracts.Types;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
@@ -21,16 +22,78 @@ public class AcceptanceTests : IClassFixture<GuestTestFixture>
|
|||||||
|
|
||||||
private HttpClient Service => fixture.Service;
|
private HttpClient Service => fixture.Service;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadSessionsPlayerCount()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var readAllResponse = await Service
|
||||||
|
.GetFromJsonAsync<ReadSessionsPlayerCountResponse>(new Uri("Sessions/PlayerCount", UriKind.Relative),
|
||||||
|
Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
readAllResponse.Should().NotBeNull();
|
||||||
|
readAllResponse!
|
||||||
|
.AllOtherSessions
|
||||||
|
.Should()
|
||||||
|
.ContainSingle(session => session.Name == "Acceptance Tests" && session.PlayerCount == 1);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Annul
|
||||||
|
await DeleteSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateAndReadSession()
|
public async Task CreateAndReadSession()
|
||||||
{
|
{
|
||||||
var createResponse = await Service.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand { Name = "Acceptance Tests" });
|
try
|
||||||
createResponse.StatusCode.Should().Be(HttpStatusCode.Created);
|
{
|
||||||
var yep = await createResponse.Content.ReadAsStringAsync();
|
// Arrange
|
||||||
console.WriteLine(yep);
|
await CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Service.GetFromJsonAsync<ReadSessionResponse>(
|
||||||
|
new Uri("Sessions/Acceptance Tests", UriKind.Relative),
|
||||||
|
Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
response.Should().NotBeNull();
|
||||||
|
response!.Session.Should().NotBeNull();
|
||||||
|
response.Session.BoardState.Board.Should().NotBeEmpty();
|
||||||
|
response.Session.BoardState.Player1Hand.Should().BeEmpty();
|
||||||
|
response.Session.BoardState.Player2Hand.Should().BeEmpty();
|
||||||
|
response.Session.BoardState.PlayerInCheck.Should().BeNull();
|
||||||
|
response.Session.BoardState.WhoseTurn.Should().Be(WhichPlayer.Player1);
|
||||||
|
response.Session.Player1.Should().NotBeNullOrEmpty();
|
||||||
|
response.Session.Player2.Should().BeNull();
|
||||||
|
response.Session.SessionName.Should().Be("Acceptance Tests");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Annul
|
||||||
|
await DeleteSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateSession()
|
||||||
|
{
|
||||||
|
var createResponse = await Service.PostAsJsonAsync(
|
||||||
|
new Uri("Sessions", UriKind.Relative),
|
||||||
|
new CreateSessionCommand { Name = "Acceptance Tests" },
|
||||||
|
Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions);
|
||||||
|
createResponse.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteSession()
|
||||||
|
{
|
||||||
|
var response = await Service.DeleteAsync(new Uri("Sessions/Acceptance Tests", UriKind.Relative));
|
||||||
|
response.StatusCode.Should().Be(HttpStatusCode.NoContent, because: "Test cleanup should succeed");
|
||||||
|
|
||||||
var readAllResponse = await Service.GetFromJsonAsync<ReadAllSessionsResponse>(new Uri("Session", UriKind.Relative));
|
|
||||||
readAllResponse.Should().NotBeNull();
|
|
||||||
readAllResponse!.AllOtherSessions.Should().ContainSingle(session => session.Name == "Acceptance Tests" && session.PlayerCount == 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,21 +21,21 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
<PackageReference Include="FluentAssertions" Version="6.8.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.46.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.48.0" />
|
||||||
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
|
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.2" />
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
<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>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,37 +1,36 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Shogi.AcceptanceTests.TestSetup
|
namespace Shogi.AcceptanceTests.TestSetup;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acceptance Test fixture for tests which assert features for Microsoft accounts.
|
||||||
|
/// </summary>
|
||||||
|
public class GuestTestFixture : IAsyncLifetime, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Acceptance Test fixture for tests which assert features for Microsoft accounts.
|
|
||||||
/// </summary>
|
|
||||||
public class GuestTestFixture : IAsyncLifetime, IDisposable
|
|
||||||
{
|
|
||||||
private bool disposedValue;
|
private bool disposedValue;
|
||||||
|
|
||||||
public GuestTestFixture()
|
public GuestTestFixture()
|
||||||
{
|
{
|
||||||
Configuration = new ConfigurationBuilder()
|
Configuration = new ConfigurationBuilder()
|
||||||
.AddJsonFile("appsettings.json")
|
.AddJsonFile("appsettings.json")
|
||||||
//.AddEnvironmentVariables()
|
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
Service = new HttpClient
|
Service = new HttpClient
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri(Configuration["ServiceUrl"], UriKind.Absolute)
|
BaseAddress = new Uri(Configuration["ServiceUrl"], UriKind.Absolute)
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IConfiguration Configuration { get; private set; }
|
public IConfiguration Configuration { get; private set; }
|
||||||
|
|
||||||
public HttpClient Service { get; }
|
public HttpClient Service { get; }
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
// Log in as a guest account.
|
// Log in as a guest account and retain the session cookie for future requests.
|
||||||
var loginResponse = await Service.GetAsync(new Uri("User/LoginAsGuest", UriKind.Relative));
|
var loginResponse = await Service.GetAsync(new Uri("User/LoginAsGuest", UriKind.Relative));
|
||||||
loginResponse.EnsureSuccessStatusCode();
|
loginResponse.IsSuccessStatusCode.Should().BeTrue(because: "Guest accounts should work");
|
||||||
|
var guestSessionCookie = loginResponse.Headers.GetValues("Set-Cookie").SingleOrDefault();
|
||||||
|
Service.DefaultRequestHeaders.Add("Set-Cookie", guestSessionCookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
@@ -58,5 +57,4 @@ namespace Shogi.AcceptanceTests.TestSetup
|
|||||||
Dispose(disposing: true);
|
Dispose(disposing: true);
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
using FluentAssertions;
|
namespace Shogi.Domain.UnitTests;
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.UnitTests
|
public class ShogiBoardStateShould
|
||||||
{
|
{
|
||||||
public class ShogiBoardStateShould
|
|
||||||
{
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void InitializeBoardState()
|
public void InitializeBoardState()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var board = new BoardState();
|
var board = BoardState.StandardStarting;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
board["A1"]?.WhichPiece.Should().Be(WhichPiece.Lance);
|
board["A1"]?.WhichPiece.Should().Be(WhichPiece.Lance);
|
||||||
@@ -182,5 +179,4 @@ namespace Shogi.Domain.UnitTests
|
|||||||
board["I9"]?.Owner.Should().Be(WhichPlayer.Player2);
|
board["I9"]?.Owner.Should().Be(WhichPlayer.Player2);
|
||||||
board["I9"]?.IsPromoted.Should().Be(false);
|
board["I9"]?.IsPromoted.Should().Be(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using Shogi.Domain.ValueObjects;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Shogi.Domain.UnitTests
|
namespace Shogi.Domain.UnitTests
|
||||||
{
|
{
|
||||||
@@ -457,6 +458,6 @@ namespace Shogi.Domain.UnitTests
|
|||||||
board.InCheck.Should().Be(WhichPlayer.Player2);
|
board.InCheck.Should().Be(WhichPlayer.Player2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ShogiBoard MockShogiBoard() => new ShogiBoard("Test Session", BoardState.StandardStarting);
|
private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
<PackageReference Include="FluentAssertions" Version="6.8.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.2" />
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
<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>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
Reference in New Issue
Block a user