Skip to content

Commit

Permalink
v3.15.9 (#376)
Browse files Browse the repository at this point in the history
* Delete external team data for prior deploy attempts on the same game.

* Rename some scoring controller methods

* Improved structure of scoring endpoint validation

* Remove import

* Fix team score test

* Really fix team score test

* Miscellaneous cleanup

* Correct game/team score endpoint validation
  • Loading branch information
sei-bstein authored Feb 14, 2024
1 parent 47a2956 commit e04825e
Show file tree
Hide file tree
Showing 11 changed files with 125 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public async Task GetTeamGameSummary_WithFixedTeamAndChallenges_CalculatesScore
(
IFixture fixture,
string gameId,
string playerId,
string playerUserId,
string teamId,
string challenge1Id,
string challenge2Id,
Expand All @@ -40,6 +42,9 @@ await _testContext.WithDataState(state =>
g.Id = gameId;
g.Players = state.Build<Data.Player>(fixture, p =>
{
p.Id = playerId;
// need to rethink default entities
p.User = state.Build<Data.User>(fixture, u => u.Id = playerUserId);
p.TeamId = teamId;
p.Challenges = new List<Data.Challenge>
{
Expand All @@ -48,6 +53,7 @@ await _testContext.WithDataState(state =>
c.Id = challenge1Id;
c.Points = baseScore1;
c.Score = baseScore1;
c.PlayerId = playerId;
c.GameId = gameId;
c.TeamId = teamId;
c.AwardedManualBonuses = new List<ManualChallengeBonus>()
Expand All @@ -73,6 +79,7 @@ await _testContext.WithDataState(state =>
c.Id = challenge2Id;
c.Points = baseScore2;
c.Score = baseScore2;
c.PlayerId = playerId;
c.GameId = gameId;
c.TeamId = teamId;
c.AwardedManualBonuses = new ManualChallengeBonus
Expand All @@ -88,8 +95,8 @@ await _testContext.WithDataState(state =>
});
});

// anon access is ok 👍
var httpClient = _testContext.CreateClient();
// only accessible by elevated roles or players on the team 👍
var httpClient = _testContext.CreateHttpClientWithActingUser(u => u.Id = playerUserId);

// when
var result = await httpClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ ITeamService teamService
public async Task CreateTeams(string gameId, IEnumerable<string> teamIds, CancellationToken cancellationToken)
{
// first, delete any metadata associated with a previous attempt
await _store
.WithNoTracking<ExternalGameTeam>()
.Where(t => t.GameId == gameId)
.ExecuteDeleteAsync(cancellationToken);

await DeleteTeamExternalData(cancellationToken, teamIds.ToArray());

// then create an entry for each team in this game
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ public class UserIsPlayingGameValidator : IGameboardValidator
private string _userId;
private readonly IStore _store;

public UserIsPlayingGameValidator(IStore store) => _store = store;
public UserIsPlayingGameValidator(IStore store)
{
_store = store;
}

public UserIsPlayingGameValidator UseGameId(string gameId)
{
Expand Down
45 changes: 41 additions & 4 deletions src/Gameboard.Api/Features/Scores/GameScoreQuery/GameScoreQuery.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,74 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Gameboard.Api.Common.Services;
using Gameboard.Api.Data;
using Gameboard.Api.Structure.MediatR;
using Gameboard.Api.Structure.MediatR.Authorizers;
using Gameboard.Api.Structure.MediatR.Validators;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Gameboard.Api.Features.Scores;

public record GameScoreQuery(string GameId) : IRequest<GameScore>;

internal sealed class GameScoreQueryHandler : IRequestHandler<GameScoreQuery, GameScore>
{
private readonly IScoringService _scoringService;

private readonly IActingUserService _actingUserService;
private readonly EntityExistsValidator<GameScoreQuery, Data.Game> _gameExists;
private readonly INowService _nowService;
private readonly IScoringService _scoringService;
private readonly IStore _store;
private readonly UserRoleAuthorizer _userRoleAuthorizer;
private readonly IValidatorService<GameScoreQuery> _validator;

public GameScoreQueryHandler
(
IActingUserService actingUserService,
EntityExistsValidator<GameScoreQuery, Data.Game> gameExists,
INowService nowService,
IScoringService scoringService,
IStore store,
UserRoleAuthorizer userRoleAuthorizer,
IValidatorService<GameScoreQuery> validator
)
{
_scoringService = scoringService;

_actingUserService = actingUserService;
_gameExists = gameExists.UseProperty(q => q.GameId);
_userRoleAuthorizer = userRoleAuthorizer;
_nowService = nowService;
_scoringService = scoringService;
_store = store;
_userRoleAuthorizer = userRoleAuthorizer;
_validator = validator;
}

public async Task<GameScore> Handle(GameScoreQuery request, CancellationToken cancellationToken)
{
// validate
_validator.AddValidator(_gameExists);

if (!_userRoleAuthorizer.AllowRoles(UserRole.Admin, UserRole.Support, UserRole.Tester, UserRole.Designer).WouldAuthorize())
{
// can only access game score details when the game is over
_validator.AddValidator(async (req, ctx) =>
{
var now = _nowService.Get();
var game = await _store
.WithNoTracking<Data.Game>()
.Select(g => new
{
g.Id,
g.GameEnd
})
.SingleOrDefaultAsync(g => g.Id == req.GameId && g.GameEnd <= now);
if (game is null)
ctx.AddValidationException(new CantAccessThisScore("game hasn't ended"));
});
}

await _validator.Validate(request, cancellationToken);

// and go
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using Gameboard.Api.Structure.MediatR.Validators;
using MediatR;
using Microsoft.EntityFrameworkCore;
using TopoMojo.Api.Client;

namespace Gameboard.Api.Features.Scores;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -107,7 +106,7 @@ private async Task RerankGame(string gameId)
.WithTracking<DenormalizedTeamScore>()
.Where(t => t.GameId == gameId)
.ToArrayAsync();
var rankedTeams = _scoringService.GetTeamkRanks(teams.Select(t => new TeamForRanking
var rankedTeams = _scoringService.GetTeamRanks(teams.Select(t => new TeamForRanking
{
CumulativeTimeMs = t.CumulativeTimeMs,
OverallScore = t.ScoreOverall,
Expand Down
4 changes: 2 additions & 2 deletions src/Gameboard.Api/Features/Scores/ScoringController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ public Task<ScoreboardData> GetScoreboard([FromRoute] string gameId)
=> _mediator.Send(new GetScoreboardQuery(gameId));

[HttpGet("challenge/{challengeId}/score")]
public async Task<TeamChallengeScore> GetTeamChallengeScoreSummary([FromRoute] string challengeId)
public async Task<TeamChallengeScore> GetChallengeScore([FromRoute] string challengeId)
=> await _mediator.Send(new TeamChallengeScoreQuery(challengeId));

[HttpGet("team/{teamId}/score")]
public async Task<TeamScoreQueryResponse> GetTeamGameScoreSummary([FromRoute] string teamId)
public async Task<TeamScoreQueryResponse> GetTeamScore([FromRoute] string teamId)
=> await _mediator.Send(new GetTeamScoreQuery(teamId));
}
12 changes: 9 additions & 3 deletions src/Gameboard.Api/Features/Scores/ScoringExceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

namespace Gameboard.Api.Features.Scores;

public sealed class CantRescoreChallengeWithANonZeroBonus : GameboardValidationException
public sealed class CantAccessThisScore : GameboardValidationException
{
public CantRescoreChallengeWithANonZeroBonus(string challengeId, string teamId, string bonusId, double pointValue)
: base($"""Challenge "{challengeId}" (for team "{teamId}") can't be re-scored, because the team has already received bonus "{bonusId}" for {pointValue} points.""") { }
public CantAccessThisScore(string message)
: base($"You don't have access to this score. This is usually because the game hasn't ended yet and you're not on an eligible team. ({message})") { }
}

public sealed class CantAwardNegativePointValue : GameboardValidationException
{
public CantAwardNegativePointValue(string challengeId, string teamId, double pointValue) : base($"""Can't award a non-positive point value ({pointValue}) to team "{teamId}" for challenge "{challengeId}".""") { }
}

public sealed class CantRescoreChallengeWithANonZeroBonus : GameboardValidationException
{
public CantRescoreChallengeWithANonZeroBonus(string challengeId, string teamId, string bonusId, double pointValue)
: base($"""Challenge "{challengeId}" (for team "{teamId}") can't be re-scored, because the team has already received bonus "{bonusId}" for {pointValue} points.""") { }
}
4 changes: 2 additions & 2 deletions src/Gameboard.Api/Features/Scores/ScoringService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public interface IScoringService
Task<GameScore> GetGameScore(string gameId, CancellationToken cancellationToken);
Task<TeamScore> GetTeamScore(string teamId, CancellationToken cancellationToken);
Task<TeamChallengeScore> GetTeamChallengeScore(string challengeId);
IDictionary<string, int> GetTeamkRanks(IEnumerable<TeamForRanking> teams);
IDictionary<string, int> GetTeamRanks(IEnumerable<TeamForRanking> teams);
}

internal class ScoringService : IScoringService
Expand Down Expand Up @@ -247,7 +247,7 @@ public async Task<TeamScore> GetTeamScore(string teamId, CancellationToken cance
};
}

public IDictionary<string, int> GetTeamkRanks(IEnumerable<TeamForRanking> teams)
public IDictionary<string, int> GetTeamRanks(IEnumerable<TeamForRanking> teams)
{
var scoreRank = 0;
TeamForRanking lastScore = null;
Expand Down
49 changes: 49 additions & 0 deletions src/Gameboard.Api/Features/Scores/TeamScoreQuery/TeamScoreQuery.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Gameboard.Api.Common.Services;
using Gameboard.Api.Data;
using Gameboard.Api.Features.Games;
using Gameboard.Api.Features.Games.Validators;
using Gameboard.Api.Features.Teams;
using Gameboard.Api.Structure.MediatR;
using Gameboard.Api.Structure.MediatR.Authorizers;
using Gameboard.Api.Structure.MediatR.Validators;
using MediatR;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;

namespace Gameboard.Api.Features.Scores;
Expand All @@ -20,29 +25,73 @@ public record GetTeamScoreQuery(string TeamId) : IRequest<TeamScoreQueryResponse

internal class GetTeamScoreHandler : IRequestHandler<GetTeamScoreQuery, TeamScoreQueryResponse>
{
private readonly IActingUserService _actingUserService;
private readonly INowService _nowService;
private readonly IScoringService _scoreService;
private readonly IStore _store;
private readonly TeamExistsValidator<GetTeamScoreQuery> _teamExists;
private readonly ITeamService _teamService;
private readonly UserRoleAuthorizer _userRoleAuthorizer;
private readonly IValidatorService<GetTeamScoreQuery> _validatorService;

public GetTeamScoreHandler(
IActingUserService actingUserService,
INowService nowService,
IScoringService scoreService,
IStore store,
TeamExistsValidator<GetTeamScoreQuery> teamExists,
ITeamService teamService,
UserRoleAuthorizer userRoleAuthorizer,
IValidatorService<GetTeamScoreQuery> validatorService)
{
_actingUserService = actingUserService;
_nowService = nowService;
_scoreService = scoreService;
_store = store;
_teamExists = teamExists;
_teamService = teamService;
_userRoleAuthorizer = userRoleAuthorizer;
_validatorService = validatorService;
}

public async Task<TeamScoreQueryResponse> Handle(GetTeamScoreQuery request, CancellationToken cancellationToken)
{
_validatorService.AddValidator(_teamExists.UseProperty(r => r.TeamId));

if (!_userRoleAuthorizer.AllowRoles(UserRole.Admin, UserRole.Designer, UserRole.Support, UserRole.Tester).WouldAuthorize())
{
_validatorService.AddValidator(async (req, ctx) =>
{
var gameInfo = await _store
.WithNoTracking<Data.Game>()
.Where(g => g.Players.Any(p => p.TeamId == req.TeamId))
.Select(g => new
{
g.Id,
g.GameEnd
})
.SingleAsync(cancellationToken);
var now = _nowService.Get();
// if the game is over, this data is generally available
if (gameInfo.GameEnd <= now)
return;
// otherwise, you need to be on the team you're looking at
var userId = _actingUserService.Get().Id;
var isOnTeam = await _store
.WithNoTracking<Data.Player>()
.Where(p => p.TeamId == request.TeamId)
.Where(p => p.UserId == userId)
.Where(p => p.GameId == gameInfo.Id)
.AnyAsync(cancellationToken);
if (!isOnTeam)
ctx.AddValidationException(new CantAccessThisScore("not on requested team"));
});
}

await _validatorService.Validate(request, cancellationToken);

// there are definitely extra reads in here but i just can't
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public async Task Validate(TModel model, CancellationToken cancellationToken)
foreach (var task in _validationTasks)
await task(model, context);

foreach (var task in _nonModelValidationTasks)
await task(context);

if (context.ValidationExceptions.Any())
{
throw GameboardAggregatedValidationExceptions.FromValidationExceptions(context.ValidationExceptions);
Expand Down

0 comments on commit e04825e

Please sign in to comment.