Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5197f4b
feat: add player scores to Match class
Dejmenek Sep 15, 2026
fb34c2f
feat: add new error codes and methods for match score validation
Dejmenek Sep 15, 2026
84209c6
feat: add score validation to Play method
Dejmenek Sep 15, 2026
fec91a9
test: add player scores to match mutation tests
Dejmenek Sep 15, 2026
1a4be26
test: add validation tests for Play mutation errors
Dejmenek Sep 15, 2026
e3207f9
test: add unit tests for MatchValidations methods
Dejmenek Sep 15, 2026
71aa408
test: add player scores to MatchNode response model class
Dejmenek Sep 15, 2026
06f271f
feat: add error for concurrent round updates
Dejmenek Sep 16, 2026
d365cca
feat: add concurrency handling when updating round
Dejmenek Sep 16, 2026
c70a00c
feat: add status to matches in BracketService
Dejmenek Sep 16, 2026
3de90ac
fix: refine match completion validation logic
Dejmenek Sep 16, 2026
97521fa
feat: add MatchStatus and update Match properties
Dejmenek Sep 16, 2026
30bd5f6
feat: add MatchCorrectionAudit entity and configuration
Dejmenek Sep 16, 2026
9ec74f1
feat: add new match correction error codes and handling methods
Dejmenek Sep 16, 2026
5ee2c67
feat: add replay handling and match correction logic
Dejmenek Sep 16, 2026
929b676
feat: add match correction and propagation logic
Dejmenek Sep 16, 2026
b6dd90c
feat: add match result correction mutation with validations
Dejmenek Sep 16, 2026
ebe75b3
feat: add MatchVersionCodec for Base64 encoding/decoding
Dejmenek Sep 16, 2026
0efbd33
refactor: add Status property to Match objects to database seeders
Dejmenek Sep 16, 2026
51b6856
test: add concurrency test for round updates
Dejmenek Sep 16, 2026
2ad6b2a
test: add tests for match correction mutations
Dejmenek Sep 16, 2026
2ea4ecd
test: add test for replaying match with stale winner
Dejmenek Sep 16, 2026
0ef0239
test: add new match metadata and result correction support
Dejmenek Sep 16, 2026
b676994
test: add unit tests for MatchCascadePositionCalculator
Dejmenek Sep 16, 2026
0748d51
test: add unit tests for MatchVersionCodec
Dejmenek Sep 16, 2026
bf285d6
test: ensure ValidateAllMatchesCompleted returns error when one of th…
Dejmenek Sep 16, 2026
5933ed8
test: add tests for MatchValidations with new statuses
Dejmenek Sep 16, 2026
d03dc41
feat: add match status filter when loading won matches by user
Dejmenek Sep 16, 2026
75acfef
docs: add reasoning behind match result correction and concurrency gu…
Dejmenek Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions TournamentAPI.Benchmarks/BenchmarkDatabaseSeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public static async Task SeedAsync(
Player1Id = selectedUsers[j * 2].Id,
Player2Id = j * 2 + 1 < selectedUsers.Count ? selectedUsers[j * 2 + 1].Id : null,
WinnerId = random.Next(2) == 0 ? selectedUsers[j * 2].Id : (j * 2 + 1 < selectedUsers.Count ? selectedUsers[j * 2 + 1].Id : selectedUsers[j * 2].Id),
Status = MatchStatus.Played,
Bracket = tournament.Bracket
};
tournament.Bracket.Matches.Add(match);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using TournamentAPI.Brackets;
using TournamentAPI.Data.Models;
using TournamentAPI.Matches;
using TournamentAPI.Shared.Models;
using TournamentAPI.Tournaments;

Expand Down Expand Up @@ -535,10 +536,10 @@ public async Task UpdateRound_HandlesDbUpdateException_WhenRaceConditionOccurs()
Assert.True(error.Extensions.ContainsKey("code"));
Assert.NotNull(error.Message);

var expectedError = BracketErrors.NextRoundAlreadyGenerated(bracketId);
Assert.Equal(expectedError.Code, error.Extensions["code"]?.ToString());
Assert.Equal(expectedError.Message, error.Message);
Assert.Equal(expectedError.Extensions!["BracketId"]!.ToString(), error.Extensions["BracketId"]?.ToString());
var code = error.Extensions["code"]?.ToString();
Assert.True(
code == BracketErrorCodes.RoundDataChanged || code == BracketErrorCodes.NextRoundAlreadyGenerated,
$"Expected either {BracketErrorCodes.RoundDataChanged} or {BracketErrorCodes.NextRoundAlreadyGenerated}, got {code}.");

var matchesInDb = await DbContext.Matches
.AsNoTracking()
Expand All @@ -548,6 +549,82 @@ public async Task UpdateRound_HandlesDbUpdateException_WhenRaceConditionOccurs()
Assert.Single(matchesInDb);
}

[Fact]
public async Task UpdateRound_RacesWithConcurrentCorrectMatchResult_LoserGetsAConcurrencyError()
{
// Arrange
var email = "carol@example.com";
var password = "Password123!";
var bracketId = 4; // tournament 7: match14, match15, round 1 only, no round 2 generated
using var client1 = CreateClient();
using var client2 = CreateClient();

var tokenResponse = await client1.ExecuteMutationAsync<LoginResponse>(
Shared.MutationExamples.Mutations.Users.LoginUser,
new
{
input = new
{
email = email,
password = password
}
});
client1.SetAuthToken(tokenResponse.Data.LoginUser.String);
client2.SetAuthToken(tokenResponse.Data.LoginUser.String);

var match14 = await DbContext.Matches.AsNoTracking().FirstAsync(m => m.Id == 14);
var version = Convert.ToBase64String(match14.RowVersion);

var updateRoundVariables = new
{
input = new
{
bracketId = bracketId,
roundNumber = 1
}
};
var correctMatchResultVariables = new
{
input = new
{
matchId = 14,
winnerId = 2,
player1Score = 1,
player2Score = 3,
version = version
}
};

// Act
var updateRoundTask = client1.ExecuteMutationAsync<UpdateRoundResponse>(
Shared.MutationExamples.Mutations.Bracket.UpdateRound,
updateRoundVariables);
var correctMatchResultTask = client2.ExecuteMutationAsync<CorrectMatchResultResponse>(
Shared.MutationExamples.Mutations.Match.CorrectMatchResult,
correctMatchResultVariables);

await Task.WhenAll(updateRoundTask, correctMatchResultTask);

var updateRoundResponse = updateRoundTask.Result;
var correctMatchResultResponse = correctMatchResultTask.Result;

// Assert: exactly one of the two concurrent mutations loses the race
Assert.True(updateRoundResponse.HasErrors ^ correctMatchResultResponse.HasErrors);

if (updateRoundResponse.HasErrors)
{
var error = updateRoundResponse.Errors!.First();
var expectedError = BracketErrors.RoundDataChangedConcurrently(bracketId);
Assert.Equal(expectedError.Code, error.Extensions!["code"]?.ToString());
}
else
{
var error = correctMatchResultResponse.Errors!.First();
var expectedError = MatchErrors.MatchVersionConflict(14);
Assert.Equal(expectedError.Code, error.Extensions!["code"]?.ToString());
}
}

[Fact]
public async Task UpdateRound_ReturnsBracketNotFoundError_WhenBracketDoesNotExist()
{
Expand Down
Loading
Loading