Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
a74cab2
feat: Don't require user session for RequestReturnedErrorCounterInc
richardscull Apr 12, 2026
f29c799
feat: GetBeatmapSet returns non nullable beatmap set
richardscull Apr 19, 2026
b50e1a3
chore: Ignore csproj.lscache
richardscull Apr 19, 2026
d9a84aa
chore: lint
richardscull Apr 19, 2026
0424ea9
feat: Add get score announcement channel method
richardscull Apr 19, 2026
8c9829f
chore: Update Sunrise.Server.csproj
richardscull Apr 19, 2026
6cbcd61
feat: Move SubmitScoreHelper and simplify
richardscull Apr 19, 2026
b66b27b
feat: Remove processing scores lock on ScoreController
richardscull Apr 19, 2026
5f8e63c
feat: Make user undefined in Score model and add scoreHash unique index
richardscull Apr 19, 2026
30bf8cf
feat: User could be null on user_stats
richardscull May 10, 2026
2a69242
feat: Add TimeElapsed to the score entity
richardscull May 10, 2026
fdad5a0
feat: Add migration to limit scorehash to 32 characters
richardscull May 10, 2026
02f674a
feat: Implement CalculateUserWeightedStats which queries acc + pp sco…
richardscull May 10, 2026
b2f9dcc
feat: Add SubmittedScore entity
richardscull May 10, 2026
5920416
feat: Use WhenPlayed for sorting grouped scores and add parsing to ba…
richardscull May 10, 2026
cd0d9b5
feat: Implement ScoreProcessingQueue and ScoreTaskQueue
richardscull May 10, 2026
be67a89
feat: Implement Sunrise.Processing.Scores
richardscull May 10, 2026
98baaba
fix: tests
richardscull May 10, 2026
9d3fa27
fix: Check if isFirstBeatmapScore to increment user stats
richardscull May 10, 2026
989d6a8
fix: UpdateWithDbScore don't update userstats pp and acc
richardscull May 10, 2026
7e15603
feat: check the grade increment based on the if better than the best …
richardscull May 16, 2026
c549f89
chore: minor improvements
richardscull May 16, 2026
50f718b
feat: Persist changes to score submission status to recalculate pp an…
richardscull May 16, 2026
a24b3b2
feat: Add Sunrise.Processing.Tests
richardscull May 16, 2026
7a458b4
fix: tests
richardscull May 16, 2026
84de90a
fix: grammar
richardscull May 23, 2026
e4bb3a8
feat: Each processor updates the db entry atomically
richardscull May 23, 2026
526c867
ref: ScoreSideEffectsPublisherService.cs
richardscull May 23, 2026
4a55d3b
ref: ScoreSideEffectsPublisherService.cs
richardscull May 23, 2026
12d3b9c
feat: Remove UpdateWithDbScore from prod
richardscull May 23, 2026
534bb83
feat: Add tests for CalculateAccuracyTests SubmittedScore
richardscull May 23, 2026
0eeea5f
feat: Reserve 0 for the Unexpected ScoreProcessingErrorCode
richardscull May 23, 2026
8c57269
feat: Use 15 secodns as starting point for ScoreProcessingBackoffSche…
richardscull May 23, 2026
ec9b20f
chore: cleanup old tests
richardscull May 23, 2026
b4be095
feat: Remove GetUnvalidatedScore
richardscull May 23, 2026
723ed85
feat: forcefully reload localproperties for the score
richardscull May 23, 2026
a43cbb3
chore: cleanup
richardscull May 29, 2026
1c42131
fix: retrieve only peers from the same gamemode
richardscull May 29, 2026
bf3afcf
chore: Add TODO
richardscull May 29, 2026
1d2108f
feat: move PrepareAsync as internal for better testability
richardscull May 31, 2026
b90cb1e
feat: make ScoreDeletionHandler override PrepareAsync to follow the p…
richardscull May 31, 2026
d9da3fb
ref: Processing handler tests
richardscull May 31, 2026
6da0b9d
feat: Add ModsValidationUtil
richardscull Jun 1, 2026
aafe910
feat: Enhance testing framework
richardscull Jun 1, 2026
83b6fec
feat: ToScore require timeElapsed + use IsModeCombinationInvalid in S…
richardscull Jun 1, 2026
b6c15b1
ref: ScoreSubmissionHandler and it's tests
richardscull Jun 1, 2026
3546863
fix: test
richardscull Jun 1, 2026
0611365
fix: test
richardscull Jun 2, 2026
0033113
fix: Don't return score result if beatmap is not scoreable
richardscull Jun 2, 2026
eb773fc
fix: update benchmark test
richardscull Jun 2, 2026
e0c7ba2
feat: Add more tests in ScoreSubmissionHandlerTests.cs
richardscull Jun 2, 2026
645a52d
feat: Set PrepareInlineSubmissionAsync as internal; add new mock
richardscull Jun 3, 2026
3431202
ref: Sunrise.Processing.Tests services and utils
richardscull Jun 3, 2026
bd89ef2
ref: test processors
richardscull Jun 6, 2026
cc92c06
feat: add missing method implementation
richardscull Jun 6, 2026
1fdaf7c
ref: score commit pipeline tests
richardscull Jun 6, 2026
28ad4f8
ref: ScoreProcessingJobTests.cs
richardscull Jun 6, 2026
e39319c
fix: Reuse scope in the test if possible in DatabaseTest
richardscull Jun 6, 2026
5e22067
fix: Disable reuse scope in same db context by default
richardscull Jun 6, 2026
a629d90
test: Add ScoreDeletionProcessingJobTests.cs
richardscull Jun 6, 2026
7fe033e
feat: Add Scores/Jobs tests
richardscull Jun 7, 2026
7ce1275
feat: Add comment explanation + error log for the pp modifiers
richardscull Jun 7, 2026
603cba5
chore: Remove tracing activity for each medal evaluation
richardscull Jun 7, 2026
378a656
feat: Update new db entity names
richardscull Jun 7, 2026
d6c29e9
fix: decrement ranked score on score deletion only if score was best …
richardscull Jun 7, 2026
6beff44
fix: misleading naming
richardscull Jun 7, 2026
38eee42
Merge branch 'version/0.2.0' into feat/add-scores-processing-service
richardscull Jun 7, 2026
6dcfec9
chore: remove doubtful TODO
richardscull Jun 7, 2026
7e276a0
chore: minor fixes
richardscull Jun 7, 2026
02bf47e
feat: Reset max combo to 0 after score deletion if its the only score…
richardscull Jun 7, 2026
aa8f3e6
fix: allow non best scores for max combo replacement
richardscull Jun 7, 2026
e800c0f
feat: Add MedalScoreProcessor.cs
richardscull Jun 7, 2026
4504ae7
fix: mods validation
richardscull Jun 7, 2026
8b8cc14
chore: Cleanup TODO and enums
richardscull Jun 7, 2026
f0ca170
feat: Add more mods validation tests
richardscull Jun 7, 2026
296478e
fix: compilation errors
richardscull Jun 7, 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Data.Tests.*

# Temporary files
*.tmp
*.csproj.lscache

# Logs
*/logs/*
Expand Down
12 changes: 7 additions & 5 deletions Sunrise.API/Controllers/ScoreController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public async Task<IActionResult> GetScore([Range(1, int.MaxValue)] int id, Cance
{
QueryModifier = query => query.Cast<Score>().IncludeUser()
},
ct);
ct: ct);

if (score == null)
return Problem(ApiErrorResponse.Detail.ScoreNotFound, statusCode: StatusCodes.Status404NotFound);
Expand All @@ -58,10 +58,12 @@ public async Task<IActionResult> GetScoreReplay([Range(1, int.MaxValue)] int id,
{
var session = HttpContext.GetCurrentSession();

var score = await database.Scores.GetScore(id, new QueryOptions(true)
{
QueryModifier = query => query.Cast<Score>().IncludeUser()
}, ct);
var score = await database.Scores.GetScore(id,
new QueryOptions(true)
{
QueryModifier = query => query.Cast<Score>().IncludeUser()
},
ct: ct);

if (score == null)
return Problem(ApiErrorResponse.Detail.ScoreNotFound, statusCode: StatusCodes.Status404NotFound);
Expand Down
3 changes: 3 additions & 0 deletions Sunrise.API/Serializable/Response/UserResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public UserResponse()

public UserResponse(SessionRepository sessionRepository, User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user), "User must be loaded before constructing UserResponse.");

var session = sessionRepository.GetSession(userId: user.Id);

Id = user.Id;
Expand Down
6 changes: 3 additions & 3 deletions Sunrise.API/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public async Task<IActionResult> SetUserAvatar(

if (!isSet || error != null)
{
SunriseMetrics.RequestReturnedErrorCounterInc(RequestType.AvatarUpload, null, error);
SunriseMetrics.RequestReturnedErrorCounterInc(RequestType.AvatarUpload, eventAction.TargetUserId, error);
return new ObjectResult(new ProblemDetails
{
Title = ApiErrorResponse.Title.UnableToChangeAvatar,
Expand Down Expand Up @@ -185,7 +185,7 @@ public async Task<IActionResult> SetUserBanner(

if (!isSet || error != null)
{
SunriseMetrics.RequestReturnedErrorCounterInc(RequestType.BannerUpload, null, error);
SunriseMetrics.RequestReturnedErrorCounterInc(RequestType.BannerUpload, eventAction.TargetUserId, error);
return new ObjectResult(new ProblemDetails
{
Title = ApiErrorResponse.Title.UnableToChangeBanner,
Expand Down Expand Up @@ -626,4 +626,4 @@ public static List<UserBadge> GetUserBadges(User user)

return badges;
}
}
}
12 changes: 12 additions & 0 deletions Sunrise.Processing.Tests/DatabaseFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Xunit;

namespace Sunrise.Processing.Tests;

public class IntegrationDatabaseFixture : Sunrise.Tests.IntegrationDatabaseFixture
{
}

[CollectionDefinition("Integration tests collection")]
public class DatabaseTestCollection : ICollectionFixture<IntegrationDatabaseFixture>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Microsoft.Extensions.DependencyInjection;
using Sunrise.Processing.Scores.Handlers;
using Sunrise.Shared.Database.Models.Scores;
using Sunrise.Shared.Enums.Scores;
using Sunrise.Tests.Abstracts;
using Sunrise.Tests.Extensions;
using Sunrise.Tests.Services.Mock;
using Xunit;
using SubmissionStatus = Sunrise.Shared.Enums.Scores.SubmissionStatus;

namespace Sunrise.Processing.Tests.Scores.Handlers;

[Collection("Integration tests collection")]
public class ScoreDeletionHandlerTests(IntegrationDatabaseFixture fixture) : DatabaseTest(fixture)
{
private readonly MockService _mocker = new();

[Fact]
public async Task TestPrepareAsyncWithMissingScoreReturnsUnexpectedError()
{
// Arrange
var handler = (ScoreDeletionHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Delete);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Delete,
ScoreId = 999_999
},
CancellationToken.None);

// Assert
Assert.True(result.IsFailure);
Assert.Equal(ScoreProcessingErrorCode.Unexpected, result.Error.Code);
}

[Fact]
public async Task TestPrepareAsyncWithAlreadyDeletedScoreReturnsFailure()
{
// Arrange
var user = await CreateTestUser();
var score = _mocker.Score.GetBestScoreableRandomScore();
score.EnrichWithUserData(user);

score.SubmissionStatus = SubmissionStatus.Deleted;

score = await CreateTestScore(score);

var handler = (ScoreDeletionHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Delete);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Delete,
ScoreId = score.Id
},
CancellationToken.None);

// Assert
Assert.True(result.IsFailure);
Assert.Equal(ScoreProcessingErrorCode.InvalidScoreState, result.Error.Code);
}

[Fact]
public async Task TestPrepareAsyncWithValidScoreReturnsDeletionContext()
{
// Arrange
var user = await CreateTestUser();
var score = _mocker.Score.GetBestScoreableRandomScore();
score.EnrichWithUserData(user);

score.SubmissionStatus = SubmissionStatus.Submitted;

score = await CreateTestScore(score);

var handler = (ScoreDeletionHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Delete);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Delete,
ScoreId = score.Id
},
CancellationToken.None);

// Assert
Assert.True(result.IsSuccess);
Assert.Equal(score.Id, result.Value.Score.Id);
Assert.Equal(user.Id, result.Value.User.Id);
Assert.Equal(user.Id, result.Value.UserStats.UserId);
Assert.Equal(user.Id, result.Value.UserGrades.UserId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
using Microsoft.Extensions.DependencyInjection;
using Sunrise.Processing.Scores.Handlers;
using Sunrise.Shared.Database.Models.Scores;
using Sunrise.Shared.Enums.Scores;
using Sunrise.Tests.Abstracts;
using Sunrise.Tests.Extensions;
using Sunrise.Tests.Services.Mock;
using Xunit;

namespace Sunrise.Processing.Tests.Scores.Handlers;

[Collection("Integration tests collection")]
public class ScoreRecalculationHandlerTests(IntegrationDatabaseFixture fixture) : DatabaseTest(fixture)
{
private readonly MockService _mocker = new();

[Fact]
public async Task TestPrepareAsyncWithMissingScoreReturnsUnexpectedError()
{
// Arrange
var handler = (ScoreRecalculationHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Recalculation);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Recalculation,
ScoreId = 999_999
},
CancellationToken.None);

// Assert
Assert.True(result.IsFailure);
Assert.Equal(ScoreProcessingErrorCode.Unexpected, result.Error.Code);
Assert.Equal(ScoreProcessingDisposition.Permanent, result.Error.Disposition);
}

[Fact]
public async Task TestPrepareAsyncWithDeletedScoreReturnsUnexpectedError()
{
// Arrange
var user = await CreateTestUser();
var score = _mocker.Score.GetBestScoreableRandomScore();
score.EnrichWithUserData(user);

score.SubmissionStatus = SubmissionStatus.Deleted;

score = await CreateTestScore(score);

var handler = (ScoreRecalculationHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Recalculation);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Recalculation,
ScoreId = score.Id
},
CancellationToken.None);

// Assert
Assert.True(result.IsFailure);
Assert.Equal(ScoreProcessingErrorCode.InvalidScoreState, result.Error.Code);
Assert.Equal(ScoreProcessingDisposition.Permanent, result.Error.Disposition);
}

[Fact]
public async Task TestPrepareAsyncWithServerErrorResponseForBeatmapReturnsBeatmapNotFoundRetryable()
{
// Arrange
var user = await CreateTestUser();
var score = _mocker.Score.GetBestScoreableRandomScore();
score.EnrichWithUserData(user);
score = await CreateTestScore(score);

App.MockHttpClient?.MockBeatmapSetByHashInternalServerError();

var handler = (ScoreRecalculationHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Recalculation);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Recalculation,
ScoreId = score.Id
},
CancellationToken.None);

// Assert
Assert.True(result.IsFailure);
Assert.Equal(ScoreProcessingErrorCode.BeatmapNotFound, result.Error.Code);
Assert.Equal(ScoreProcessingDisposition.Retryable, result.Error.Disposition);
}

[Fact]
public async Task TestPrepareAsyncWithMissingBeatmapReturnsBeatmapNotFoundPermanent()
{
// Arrange
var user = await CreateTestUser();
var score = _mocker.Score.GetBestScoreableRandomScore();
score.EnrichWithUserData(user);
score = await CreateTestScore(score);

App.MockHttpClient?.MockBeatmapSetByBeatmapIdNotFound(score.BeatmapId);

var handler = (ScoreRecalculationHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Recalculation);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Recalculation,
ScoreId = score.Id
},
CancellationToken.None);

// Assert
Assert.True(result.IsFailure);
Assert.Equal(ScoreProcessingErrorCode.BeatmapNotFound, result.Error.Code);
Assert.Equal(ScoreProcessingDisposition.Permanent, result.Error.Disposition);
}

[Fact]
public async Task TestPrepareAsyncWithFailedPpCalculationReturnsPpCalculationFailed()
{
// Arrange
var user = await CreateTestUser();
var score = _mocker.Score.GetBestScoreableRandomScore();
score.EnrichWithUserData(user);
score = await CreateTestScore(score);

await _mocker.Beatmap.MockRankedBeatmapWithSetForScore(score);

var handler = (ScoreRecalculationHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Recalculation);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Recalculation,
ScoreId = score.Id
},
CancellationToken.None);

// Assert
Assert.True(result.IsFailure);
Assert.Equal(ScoreProcessingErrorCode.PpCalculationFailed, result.Error.Code);
Assert.Equal(ScoreProcessingDisposition.Retryable, result.Error.Disposition);
}

[Fact]
public async Task TestPrepareAsyncWithExistingScoreReturnsContextWithRecalculatedPerformance()
{
// Arrange
var user = await CreateTestUser();
var score = _mocker.Score.GetBestScoreableRandomScore();
score.EnrichWithUserData(user);

score.PerformancePoints = 123;

score = await CreateTestScore(score);

var (_, beatmap) = await _mocker.Beatmap.MockRankedBeatmapWithSetForScore(score);
App.MockHttpClient?.MockPerformanceCalculation(performancePoints: 321);

var handler = (ScoreRecalculationHandler)Scope.ServiceProvider
.GetRequiredKeyedService<IScoreHandler>(ScoreTaskType.Recalculation);

// Act
var result = await handler.PrepareAsync(new ScoreProcessingTask
{
TaskType = ScoreTaskType.Recalculation,
ScoreId = score.Id
},
CancellationToken.None);

// Assert
Assert.True(result.IsSuccess);

Assert.Equal(ScoreTaskType.Recalculation, result.Value.TaskType);
Assert.Equal(score.Id, result.Value.Score.Id);
Assert.Equal(321, result.Value.Score.PerformancePoints);

Assert.NotNull(result.Value.Beatmap);
Assert.Equal(beatmap.Checksum, result.Value.Beatmap!.Checksum);
}
}
Loading
Loading