diff --git a/src/Gameboard.Api.Tests.Integration/Fixtures/Extensions/AssertionExtensions.cs b/src/Gameboard.Api.Tests.Integration/Fixtures/Extensions/AssertionExtensions.cs index 4091a518..0715ad87 100644 --- a/src/Gameboard.Api.Tests.Integration/Fixtures/Extensions/AssertionExtensions.cs +++ b/src/Gameboard.Api.Tests.Integration/Fixtures/Extensions/AssertionExtensions.cs @@ -15,6 +15,7 @@ public static async Task ShouldYieldGameboardValidationException(this Task(responseContent)) throw new WrongExceptionType(typeof(T), responseContent); } diff --git a/src/Gameboard.Api.Tests.Integration/Tests/Features/Teams/StartTeamSessionTests.cs b/src/Gameboard.Api.Tests.Integration/Tests/Features/Teams/StartTeamSessionTests.cs new file mode 100644 index 00000000..b9b7b16f --- /dev/null +++ b/src/Gameboard.Api.Tests.Integration/Tests/Features/Teams/StartTeamSessionTests.cs @@ -0,0 +1,94 @@ +using Gameboard.Api.Common; +using Gameboard.Api.Structure; + +namespace Gameboard.Api.Tests.Integration.Teams; + +public class TeamControllerStartTeamSessionTests(GameboardTestContext testContext) : IClassFixture +{ + private readonly GameboardTestContext _testContext = testContext; + + [Theory, GbIntegrationAutoData] + public async Task TeamGame_WithSinglePlayer_CantStart + ( + string gameId, + string playerId, + string userId, + IFixture fixture + ) + { + // given a team game and a registered player with no teammates + await _testContext.WithDataState(state => + { + state.Add(new Data.Game + { + Id = gameId, + MinTeamSize = 2, + MaxTeamSize = 5, + GameStart = DateTimeOffset.UtcNow, + GameEnd = DateTimeOffset.UtcNow.AddDays(1), + Mode = GameEngineMode.Standard, + Players = state.Build(fixture, p => + { + p.Id = playerId; + p.User = state.Build(fixture, u => u.Id = userId); + }).ToCollection() + }); + }); + + // when they try to start their session + await _testContext + .CreateHttpClientWithActingUser(u => u.Id = userId) + .PutAsync($"api/player/{playerId}/start", null) + // they should get a validation error + .ShouldYieldGameboardValidationException(); + } + + [Theory, GbIntegrationAutoData] + public async Task TeamGame_WithTwoPlayers_CanStart + ( + string gameId, + string playerId, + string userId, + string teamId, + IFixture fixture + ) + { + // given a team game and a registered player with no teammates + await _testContext.WithDataState(state => + { + state.Add(new Data.Game + { + Id = gameId, + MinTeamSize = 2, + MaxTeamSize = 5, + GameStart = DateTimeOffset.UtcNow, + GameEnd = DateTimeOffset.UtcNow.AddDays(1), + Mode = GameEngineMode.Standard, + Players = + [ + state.Build(fixture, p => + { + p.Id = playerId; + p.Role = PlayerRole.Manager; + p.TeamId = teamId; + p.User = state.Build(fixture, u => u.Id = userId); + }), + state.Build(fixture, p => + { + p.Id = fixture.Create(); + p.Role = PlayerRole.Member; + p.TeamId = teamId; + }) + ] + }); + }); + + // when they try to start their session + var result = await _testContext + .CreateHttpClientWithActingUser(u => u.Id = userId) + .PutAsync($"api/player/{playerId}/start", null) + .DeserializeResponseAs(); + + // + } +} diff --git a/src/Gameboard.Api/Extensions/ExceptionExtensions.cs b/src/Gameboard.Api/Extensions/ExceptionExtensions.cs index 4c13631f..9aa938a8 100644 --- a/src/Gameboard.Api/Extensions/ExceptionExtensions.cs +++ b/src/Gameboard.Api/Extensions/ExceptionExtensions.cs @@ -6,6 +6,9 @@ namespace Gameboard.Api; public static class ExceptionExtensions { + public static string ToTypeName(string exceptionCode) + => Encoding.UTF8.GetString(Convert.FromBase64String(exceptionCode)); + public static string ToExceptionCode(this T exception) where T : GameboardValidationException => ToExceptionCode(typeof(T)); diff --git a/src/Gameboard.Api/Features/Player/Services/PlayerService.cs b/src/Gameboard.Api/Features/Player/Services/PlayerService.cs index 6e6050a0..0a64ac8f 100644 --- a/src/Gameboard.Api/Features/Player/Services/PlayerService.cs +++ b/src/Gameboard.Api/Features/Player/Services/PlayerService.cs @@ -219,7 +219,7 @@ public async Task StartSession(SessionStartRequest model, User actor, bo .WithNoTracking() .SingleOrDefaultAsync(p => p.Id == model.PlayerId); - var result = await _mediator.Send(new StartTeamSessionsCommand(new string[] { startingPlayer.TeamId })); + var result = await _mediator.Send(new StartTeamSessionsCommand([startingPlayer.TeamId])); // also set the starting player's properties because we'll use them as a return var teamStartResult = result.Teams[startingPlayer.TeamId]; diff --git a/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessions.cs b/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessions.cs index eec7dedf..39e0d3f9 100644 --- a/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessions.cs +++ b/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessions.cs @@ -20,7 +20,8 @@ namespace Gameboard.Api.Features.Teams; public record StartTeamSessionsCommand(IEnumerable TeamIds) : IRequest; -internal sealed class StartTeamSessionsHandler( +internal sealed class StartTeamSessionsHandler +( IActingUserService actingUserService, IExternalGameHostService externalGameHostService, IGameModeServiceFactory gameModeServiceFactory, @@ -35,7 +36,7 @@ internal sealed class StartTeamSessionsHandler( ITeamService teamService, IUserRolePermissionsService permissionsService, IGameboardRequestValidator validator - ) : IRequestHandler +) : IRequestHandler { private readonly User _actingUser = actingUserService.Get(); private readonly IExternalGameHostService _externalGameHostService = externalGameHostService; @@ -59,7 +60,7 @@ public async Task Handle(StartTeamSessionsCommand reque // throw on cancel request so we can clean up the debris cancellationToken.ThrowIfCancellationRequested(); - _logger.LogInformation($"Gathering data for game start (teams: {request.TeamIds.ToDelimited()})", "resolving game..."); + _logger.LogInformation($"Gathering data for game start (teams: {request.TeamIds.ToDelimited()})"); var teams = await _store .WithNoTracking() .Where(p => request.TeamIds.Contains(p.TeamId)) diff --git a/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessionsValidator.cs b/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessionsValidator.cs index 1fc28c63..0f2926ae 100644 --- a/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessionsValidator.cs +++ b/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessionsValidator.cs @@ -14,38 +14,26 @@ namespace Gameboard.Api.Features.Teams; -internal class StartTeamSessionsValidator : IGameboardRequestValidator +internal class StartTeamSessionsValidator +( + IActingUserService actingUserService, + IGameService gameService, + IGameModeServiceFactory gameModeServiceFactory, + INowService now, + IUserRolePermissionsService permissionsService, + ISessionWindowCalculator sessionWindow, + IStore store, + IValidatorService validatorService +) : IGameboardRequestValidator { - private readonly User _actingUser; - private readonly IGameModeServiceFactory _gameModeServiceFactory; - private readonly IGameService _gameService; - private readonly INowService _now; - private readonly IUserRolePermissionsService _permissionsService; - private readonly ISessionWindowCalculator _sessionWindow; - private readonly IStore _store; - private readonly IValidatorService _validatorService; - - public StartTeamSessionsValidator - ( - IActingUserService actingUserService, - IGameService gameService, - IGameModeServiceFactory gameModeServiceFactory, - INowService now, - IUserRolePermissionsService permissionsService, - ISessionWindowCalculator sessionWindow, - IStore store, - IValidatorService validatorService - ) - { - _actingUser = actingUserService.Get(); - _gameModeServiceFactory = gameModeServiceFactory; - _gameService = gameService; - _now = now; - _permissionsService = permissionsService; - _sessionWindow = sessionWindow; - _store = store; - _validatorService = validatorService; - } + private readonly User _actingUser = actingUserService.Get(); + private readonly IGameModeServiceFactory _gameModeServiceFactory = gameModeServiceFactory; + private readonly IGameService _gameService = gameService; + private readonly INowService _now = now; + private readonly IUserRolePermissionsService _permissionsService = permissionsService; + private readonly ISessionWindowCalculator _sessionWindow = sessionWindow; + private readonly IStore _store = store; + private readonly IValidatorService _validatorService = validatorService; public async Task Validate(StartTeamSessionsCommand request, CancellationToken cancellationToken) {