Skip to content

Commit

Permalink
Various bug fixes for competitive and version 3.25.0
Browse files Browse the repository at this point in the history
  • Loading branch information
sei-bstein committed Nov 19, 2024
1 parent a554d7e commit e6da262
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,6 @@ namespace Gameboard.Api.Tests.Unit;

public class PlayerServiceTests
{
[Theory, GameboardAutoData]
public async Task Standings_WhenGameIdIsEmpty_ReturnsEmptyArray(IFixture fixture)
{
// arrange
var sut = fixture.Create<PlayerService>();
var filterParams = A.Fake<PlayerDataFilter>();

// act
var result = await sut.Standings(filterParams);

// assert
result.ShouldBe(Array.Empty<Standing>());
}

[Theory, GameboardAutoData]
public async Task MakeCertificates_WhenScoreZero_ReturnsEmptyArray(IFixture fixture)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Security.Cryptography.Pkcs;
using AutoMapper;
using Gameboard.Api.Common;
using Gameboard.Api.Common.Services;
using Gameboard.Api.Data;
using Gameboard.Api.Features.Challenges;
using Gameboard.Api.Features.Practice;
using Gameboard.Api.Features.Users;
using Microsoft.EntityFrameworkCore;

namespace Gameboard.Api.Tests.Unit;
Expand Down Expand Up @@ -32,9 +34,8 @@ public async Task SearchPracticeChallenges_WithDisabled_ReturnsEmpty(IFixture fi
var sut = GetSutWithResults(fixture, disabledSpec);

// when a query for all challenges is issued
var result = await sut
.BuildQuery(string.Empty, Array.Empty<string>())
.ToArrayAsync(CancellationToken.None);
var query = await sut.BuildQuery(string.Empty, []);
var result = await query.ToArrayAsync(CancellationToken.None);

// then we expect no results
result.Length.ShouldBe(0);
Expand All @@ -61,9 +62,8 @@ public async Task SearchPracticeChallenges_WithEnabled_Returns(IFixture fixture)
var sut = GetSutWithResults(fixture, enabledSpec);

// when a query for all challenges is issued
var result = await sut
.BuildQuery(string.Empty, Array.Empty<string>())
.ToArrayAsync(CancellationToken.None);
var query = await sut.BuildQuery(string.Empty, []);
var result = await query.ToArrayAsync(CancellationToken.None);

// then we expect one result
result.Length.ShouldBe(1);
Expand All @@ -81,8 +81,8 @@ private SearchPracticeChallengesHandler GetSutWithResults(IFixture fixture, para
var sut = new SearchPracticeChallengesHandler
(
A.Fake<IChallengeDocsService>(),
A.Fake<IMapper>(),
A.Fake<IPagingService>(),
A.Fake<IUserRolePermissionsService>(),
A.Fake<IPracticeService>(),
A.Fake<ISlugService>(),
store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ await _validator
// aggregates
ChallengeCount = challengeData?.ChallengeCount ?? 0,
PointsAvailable = challengeData?.PointsAvailable ?? 0,
OpenTicketCount = gameTotalTicketCount,
OpenTicketCount = gameOpenTicketCount,
TotalTicketCount = gameTotalTicketCount
};
}
Expand Down
8 changes: 3 additions & 5 deletions src/Gameboard.Api/Features/Challenge/ChallengeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,13 @@ public ChallengeMapper()
.ForMember(d => d.LastScoreTime, opt => opt.MapFrom(s => s.Challenge.LastScoreTime))
.ForMember(d => d.Score, opt => opt.MapFrom(s => s.Challenge.Score))
.ForMember(d => d.HasDeployedGamespace, opt => opt.MapFrom(s => s.Vms != null && s.Vms.Any()))
.ForMember(d => d.State, opt => opt.MapFrom(s => JsonSerializer.Serialize(s, JsonOptions)))
;
.ForMember(d => d.State, opt => opt.MapFrom(s => JsonSerializer.Serialize(s, JsonOptions)));

CreateMap<Data.Challenge, Challenge>()
.ForMember(d => d.Score, opt => opt.MapFrom(s => (int)Math.Floor(s.Score)))
.ForMember(d => d.State, opt => opt.MapFrom(s =>
JsonSerializer.Deserialize<GameEngineGameState>(s.State, JsonOptions))
)
;
);

CreateMap<Data.Player, ChallengePlayer>()
.ForMember(cp => cp.IsManager, o => o.MapFrom(p => p.Role == PlayerRole.Manager));
Expand All @@ -67,7 +65,7 @@ public ChallengeMapper()
.ForMember(d => d.Events, o => o.MapFrom(c => c.Events.OrderBy(e => e.Timestamp)))
.ForMember(s => s.Players, o => o.MapFrom(d => new ChallengePlayer[]
{
new ChallengePlayer
new()
{
Id = d.PlayerId,
Name = d.Player.Name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ public ChallengeSpecMapper()
private static partial Regex TagsSplitRegex();

// EF advises to make this mapping a static method to avoid memory leaks
private static IEnumerable<string> StringTagsToEnumerableStringTags(string tagsIn)
public static IEnumerable<string> StringTagsToEnumerableStringTags(string tagsIn)
{
if (tagsIn.IsEmpty())
return Array.Empty<string>();
return [];

return TagsSplitRegex().Split(tagsIn);
}
Expand Down
7 changes: 3 additions & 4 deletions src/Gameboard.Api/Features/Player/PlayerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ public async Task<PlayerUpdatedViewModel> Update([FromBody] ChangedPlayer model)
{
await AuthorizeAny
(
() => _permissionsService.IsActingUserAsync(model.Id),
() => IsSelf(model.Id),
() => _permissionsService.Can(PermissionKey.Teams_ApproveNameChanges)
);

await Validate(model);

var result = await PlayerService.Update(model, Actor, await _permissionsService.Can(PermissionKey.Teams_ApproveNameChanges));
var result = await PlayerService.Update(model, Actor);
return Mapper.Map<PlayerUpdatedViewModel>(result);
}

Expand Down Expand Up @@ -120,12 +120,11 @@ await AuthorizeAny
/// Delete a player enrollment
/// </summary>
/// <param name="playerId"></param>
/// <param name="asAdmin"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpDelete("/api/player/{playerId}")]
[Authorize]
public async Task Unenroll([FromRoute] string playerId, [FromQuery] bool asAdmin, CancellationToken cancellationToken)
public async Task Unenroll([FromRoute] string playerId, CancellationToken cancellationToken)
{
await AuthorizeAny
(
Expand Down
20 changes: 14 additions & 6 deletions src/Gameboard.Api/Features/Player/PlayerMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,22 @@ public PlayerMapper()
.ForMember(vm => vm.PreUpdateName, opts => opts.Ignore());

CreateMap<Data.Player, Team>()
.AfterMap((player, team) => team.Members = new List<TeamMember>
.AfterMap((player, team) =>
{
new()
team.Members =
[
new()
{
Id = player.Id,
ApprovedName = player.ApprovedName,
Role = player.Role,
UserId = player.UserId
}
];
if (team.Members.Any() && !team.Members.Any(p => p.Role == PlayerRole.Manager))
{
Id = player.Id,
ApprovedName = player.ApprovedName,
Role = player.Role,
UserId = player.UserId
team.Members.OrderBy(p => p.ApprovedName).First().Role = PlayerRole.Manager;
}
});
}
Expand Down
62 changes: 16 additions & 46 deletions src/Gameboard.Api/Features/Player/Services/PlayerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,17 @@ public async Task<Player> Retrieve(string id)
return _mapper.Map<Player>(await _store.WithNoTracking<Data.Player>().SingleAsync(p => p.Id == id));
}

public async Task<Player> Update(ChangedPlayer model, User actor, bool sudo = false)
public async Task<Player> Update(ChangedPlayer model, User actor)
{
var player = await _store
.WithNoTracking<Data.Player>()
.SingleAsync(p => p.Id == model.Id);
var prev = _mapper.Map<Player>(player);

if (!sudo)
// people with the appropriate permissions can hard-set their names
if (!await _permissionsService.Can(PermissionKey.Teams_ApproveNameChanges))
{
_mapper.Map(
_mapper.Map<SelfChangedPlayer>(model),
player
);
_mapper.Map(_mapper.Map<SelfChangedPlayer>(model), player);
}
else
{
Expand All @@ -189,13 +187,15 @@ public async Task<Player> Update(ChangedPlayer model, User actor, bool sudo = fa
if (prev.Name != player.Name)
{
// check uniqueness
bool found = await _store
var found = await _store
.WithNoTracking<Data.Player>()
.AnyAsync(p =>
p.GameId == player.GameId &&
p.TeamId != player.TeamId &&
p.Name == player.Name
);
.AnyAsync
(
p =>
p.GameId == player.GameId &&
p.TeamId != player.TeamId &&
(p.Name == player.Name || p.ApprovedName == player.Name)
);

if (found)
player.NameStatus = AppConstants.NameStatusNotUnique;
Expand Down Expand Up @@ -264,36 +264,6 @@ public async Task<Player[]> List(PlayerDataFilter model, bool sudo = false)
return players;
}

public async Task<Standing[]> Standings(PlayerDataFilter model)
{
if (model.gid.IsEmpty())
return [];

model.Filter = [.. model.Filter, PlayerDataFilter.FilterScoredOnly];
model.mode = PlayerMode.Competition.ToString();
var q = BuildListQuery(model);
var standings = await _mapper.ProjectTo<Standing>(q).ToArrayAsync();

// as a temporary workaround until we get the new scoreboard, we need to manually
// set the Sponsors property to accommodate multisponsor teams.
var allTeamIds = standings.Select(s => s.TeamId);
var allSponsors = await _store.WithNoTracking<Data.Sponsor>()
.ToDictionaryAsync(s => s.Id, s => s);

var teamsWithSponsors = await _store
.WithNoTracking<Data.Player>()
.Where(p => allTeamIds.Contains(p.TeamId))
.GroupBy(p => p.TeamId)
.ToDictionaryAsync(g => g.Key, g => g.Select(p => p.SponsorId).ToArray());

foreach (var standing in standings)
{
var distinctSponsors = teamsWithSponsors[standing.TeamId].Distinct().Select(s => allSponsors[s]);
standing.TeamSponsors = _mapper.Map<Sponsor[]>(distinctSponsors);
}
return standings;
}

private IQueryable<Data.Player> BuildListQuery(PlayerDataFilter model)
{
var ts = _now.Get();
Expand All @@ -303,7 +273,7 @@ public async Task<Standing[]> Standings(PlayerDataFilter model)
.Include(p => p.User)
.Include(p => p.Sponsor)
.Include(p => p.AdvancedFromGame)
.AsNoTracking();
.AsQueryable();

if (model.WantsMode)
q = q.Where(p => p.Mode == Enum.Parse<PlayerMode>(model.mode, true));
Expand Down Expand Up @@ -443,11 +413,11 @@ public async Task<TeamInvitation> GenerateInvitation(string id)
if (player.Role != PlayerRole.Manager)
throw new ActionForbidden();

byte[] buffer = new byte[16];

var buffer = new byte[16];
new Random().NextBytes(buffer);

var code = Convert.ToBase64String(buffer)
var code = Convert
.ToBase64String(buffer)
.Replace("+", string.Empty)
.Replace("/", string.Empty)
.Replace("=", string.Empty);
Expand Down
5 changes: 0 additions & 5 deletions src/Gameboard.Api/Features/Practice/PracticeModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ public sealed class PracticeSession
public required string UserId { get; set; }
}

public sealed class SearchPracticeChallengesResult
{
public required PagedEnumerable<ChallengeSpecSummary> Results { get; set; }
}

public sealed class PracticeModeSettingsApiModel
{
public int? AttemptLimit { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Gameboard.Api.Common.Services;
using Gameboard.Api.Data;
using Gameboard.Api.Features.Challenges;
using Gameboard.Api.Features.Users;
using Gameboard.Api.Services;
using MediatR;
using Microsoft.EntityFrameworkCore;

Expand All @@ -16,16 +17,16 @@ public record SearchPracticeChallengesQuery(SearchFilter Filter) : IRequest<Sear
internal class SearchPracticeChallengesHandler
(
IChallengeDocsService challengeDocsService,
IMapper mapper,
IPagingService pagingService,
IUserRolePermissionsService permissionsService,
IPracticeService practiceService,
ISlugService slugger,
IStore store
) : IRequestHandler<SearchPracticeChallengesQuery, SearchPracticeChallengesResult>
{
private readonly IChallengeDocsService _challengeDocsService = challengeDocsService;
private readonly IMapper _mapper = mapper;
private readonly IPagingService _pagingService = pagingService;
private readonly IUserRolePermissionsService _permissionsService = permissionsService;
private readonly IPracticeService _practiceService = practiceService;
private readonly ISlugService _slugger = slugger;
private readonly IStore _store = store;
Expand All @@ -36,8 +37,28 @@ public async Task<SearchPracticeChallengesResult> Handle(SearchPracticeChallenge
var settings = await _practiceService.GetSettings(cancellationToken);
var sluggedSuggestedSearches = settings.SuggestedSearches.Select(search => _slugger.Get(search));

var query = BuildQuery(request.Filter.Term, sluggedSuggestedSearches);
var results = await _mapper.ProjectTo<ChallengeSpecSummary>(query).ToArrayAsync(cancellationToken);
var query = await BuildQuery(request.Filter.Term, sluggedSuggestedSearches);
var results = await _store
.WithNoTracking<Data.ChallengeSpec>()
.Select(s => new PracticeChallengeView
{
Id = s.Id,
Name = s.Name,
Description = s.Description,
Text = s.Text,
AverageDeploySeconds = s.AverageDeploySeconds,
IsHidden = s.IsHidden,
SolutionGuideUrl = s.SolutionGuideUrl,
Tags = ChallengeSpecMapper.StringTagsToEnumerableStringTags(s.Tags),
Game = new PracticeChallengeViewGame
{
Id = s.Game.Id,
Name = s.Game.Name,
Logo = s.Game.Logo,
IsHidden = !s.Game.IsPublished
}
})
.ToArrayAsync(cancellationToken);

foreach (var result in results)
{
Expand Down Expand Up @@ -69,14 +90,23 @@ public async Task<SearchPracticeChallengesResult> Handle(SearchPracticeChallenge
/// <param name="filterTerm"></param>
/// <param name="sluggedSuggestedSearches"></param>
/// <returns></returns>
internal IQueryable<Data.ChallengeSpec> BuildQuery(string filterTerm, IEnumerable<string> sluggedSuggestedSearches)
internal async Task<IQueryable<Data.ChallengeSpec>> BuildQuery(string filterTerm, IEnumerable<string> sluggedSuggestedSearches)
{
var canViewHidden = await _permissionsService.Can(PermissionKey.Games_ViewUnpublished);

var q = _store
.WithNoTracking<Data.ChallengeSpec>()
.Include(s => s.Game)
.Where(s => s.Game.PlayerMode == PlayerMode.Practice)
.Where(s => !s.Disabled)
.Where(s => !s.IsHidden);
.Where(s => !s.Disabled);

if (!canViewHidden)
{
// without the permission, neither spec nor the game can be hidden
q = q
.Where(s => !s.IsHidden)
.Where(g => !g.IsHidden);
}

if (filterTerm.IsNotEmpty())
{
Expand Down
Loading

0 comments on commit e6da262

Please sign in to comment.