Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion src/Game.Server/Controllers/RankingsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Game.Server.Dto.Responses;
using Game.Server.Services.Interfaces;
using Game.Server.Shared.Extensions;
using Game.Server.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -12,10 +13,12 @@ namespace Game.Server.Controllers;
public class RankingsController : ControllerBase
{
private readonly IRankingService _rankingService;
private readonly ISurvivorValidator _survivorValidator;

public RankingsController(IRankingService rankingService)
public RankingsController(IRankingService rankingService, ISurvivorValidator survivorValidator)
{
_rankingService = rankingService;
_survivorValidator = survivorValidator;
}

[HttpGet("{stageId:int}")]
Expand All @@ -25,6 +28,10 @@ public async Task<IActionResult> GetRanking(
[FromQuery] int limit = 100,
[FromQuery] int offset = 0)
{
_survivorValidator.ValidateStageId(stageId);
_survivorValidator.ValidateLimit(limit);
_survivorValidator.ValidateOffset(offset);

var result = await _rankingService.GetRankingAsync(stageId, limit, offset);
return Ok(result);
}
Expand All @@ -35,6 +42,8 @@ public async Task<IActionResult> GetRanking(
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetMyRank(int stageId)
{
_survivorValidator.ValidateStageId(stageId);

if (!Guid.TryParse(User.GetUserId(), out var userId))
{
return Unauthorized();
Expand Down
11 changes: 10 additions & 1 deletion src/Game.Server/Controllers/SurvivorScoresController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Game.Server.Dto.Responses;
using Game.Server.Services.Interfaces;
using Game.Server.Shared.Extensions;
using Game.Server.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -13,10 +14,12 @@ namespace Game.Server.Controllers;
public class SurvivorScoresController : ControllerBase
{
private readonly ISurvivorScoreService _scoreService;
private readonly ISurvivorValidator _survivorValidator;

public SurvivorScoresController(ISurvivorScoreService scoreService)
public SurvivorScoresController(ISurvivorScoreService scoreService, ISurvivorValidator survivorValidator)
{
_scoreService = scoreService;
_survivorValidator = survivorValidator;
}

[HttpPost]
Expand All @@ -42,6 +45,12 @@ public async Task<IActionResult> GetMyScores(
[FromQuery] int? stageId = null,
[FromQuery] int limit = 50)
{
if (stageId.HasValue)
{
_survivorValidator.ValidateStageId(stageId.Value);
}
_survivorValidator.ValidateLimit(limit);

if (!Guid.TryParse(User.GetUserId(), out var userId))
{
return Unauthorized();
Expand Down
2 changes: 1 addition & 1 deletion src/Game.Server/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public static IServiceCollection AddApplicationServices(
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IRankingService, RankingService>();
services.AddScoped<ISurvivorScoreValidator, SurvivorScoreValidator>();
services.AddScoped<ISurvivorValidator, SurvivorValidator>();
services.AddScoped<ISurvivorScoreService, SurvivorScoreService>();

// Repositories
Expand Down
8 changes: 4 additions & 4 deletions src/Game.Server/Services/SurvivorScoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@
private readonly ISurvivorScoreRepository _scoreRepository;
private readonly IRankingRepository _rankingRepository;
private readonly IRankingService _rankingService;
private readonly ISurvivorScoreValidator _survivorScoreValidator;
private readonly ISurvivorValidator _survivorValidator;

public SurvivorScoreService(
ISurvivorScoreRepository scoreRepository,
IRankingRepository rankingRepository,
IRankingService rankingService,
ISurvivorScoreValidator survivorScoreValidator)
ISurvivorValidator survivorValidator)
{
_scoreRepository = scoreRepository;
_rankingRepository = rankingRepository;
_rankingService = rankingService;
_survivorScoreValidator = survivorScoreValidator;
_survivorValidator = survivorValidator;
}

public async Task<Result<SurvivorScoreSubmitResponse, ApiError>> SubmitScoreAsync(
Guid userId, ScoreSubmitDto request)
{
_survivorScoreValidator.Validate(request);
_survivorValidator.ValidateScoreSubmit(request);
// ErrorException("INVALID_SCORE") は ExceptionHandlingMiddleware が処理

Check warning on line 33 in src/Game.Server/Services/SurvivorScoreService.cs

View workflow job for this annotation

GitHub Actions / dotnet format Check

Single-line comment should be preceded by blank line

Check warning on line 33 in src/Game.Server/Services/SurvivorScoreService.cs

View workflow job for this annotation

GitHub Actions / dotnet format Check

Single-line comments should not be followed by blank line

var previousBest = await _rankingRepository.GetUserBestScoreAsync(
request.StageId, userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,30 @@

namespace Game.Server.Validation;

public interface ISurvivorScoreValidator
public interface ISurvivorValidator
{
void Validate(ScoreSubmitDto request);
void ValidateScoreSubmit(ScoreSubmitDto request);
void ValidateStageId(int stageId);
void ValidateLimit(int limit);
void ValidateOffset(int offset);
}

public class SurvivorScoreValidator : ISurvivorScoreValidator
public class SurvivorValidator : ISurvivorValidator
{
private const float ClearTimeBufferSeconds = 5f;
private const int MaxLimit = 1000;
private const int MaxOffset = 100000;

private readonly IMasterDataService _masterData;
private readonly ILogger<SurvivorScoreValidator> _logger;
private readonly ILogger<SurvivorValidator> _logger;

public SurvivorScoreValidator(IMasterDataService masterData, ILogger<SurvivorScoreValidator> logger)
public SurvivorValidator(IMasterDataService masterData, ILogger<SurvivorValidator> logger)
{
_masterData = masterData;
_logger = logger;
}

public void Validate(ScoreSubmitDto request)
public void ValidateScoreSubmit(ScoreSubmitDto request)
{
// 1. ステージ存在チェック
if (!_masterData.MemoryDatabase.SurvivorStageMasterTable.TryFindById(request.StageId, out var stage))
Expand Down Expand Up @@ -93,4 +98,30 @@ public void Validate(ScoreSubmitDto request)
throw new ErrorException("INVALID_SCORE", "Score exceeds maximum possible value.");
}
}

public void ValidateStageId(int stageId)
{
if (stageId <= 0)
{
throw new ErrorException("INVALID_INPUT", "Stage ID must be greater than 0.");
}
}

public void ValidateLimit(int limit)
{
if (limit < 1 || limit > MaxLimit)
{
throw new ErrorException("INVALID_INPUT",
$"Limit must be between 1 and {MaxLimit}.");
}
}

public void ValidateOffset(int offset)
{
if (offset < 0 || offset > MaxOffset)
{
throw new ErrorException("INVALID_INPUT",
$"Offset must be between 0 and {MaxOffset}.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
services.RemoveAll<IMasterDataService>();
services.AddSingleton(mockMasterData.Object);

// Replace ISurvivorScoreValidator with a mock so that tests
// don't require master data for score validation.
var mockValidation = new Mock<ISurvivorScoreValidator>();
services.RemoveAll<ISurvivorScoreValidator>();
// Replace ISurvivorValidator with a mock so that tests
// don't require master data for survivor validation.
var mockValidation = new Mock<ISurvivorValidator>();
services.RemoveAll<ISurvivorValidator>();
services.AddSingleton(mockValidation.Object);

// Replace IConnectionMultiplexer with a mock so that tests
Expand Down
6 changes: 3 additions & 3 deletions test/Game.Server.Tests/Integration/TestServiceOverrides.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public static void Apply(IServiceCollection services)
services.RemoveAll<IMasterDataService>();
services.AddSingleton(mockMasterData.Object);

// ScoreValidator
var mockValidation = new Mock<ISurvivorScoreValidator>();
services.RemoveAll<ISurvivorScoreValidator>();
// SurvivorValidator
var mockValidation = new Mock<ISurvivorValidator>();
services.RemoveAll<ISurvivorValidator>();
services.AddSingleton(mockValidation.Object);

// Valkey/Redis
Expand Down
4 changes: 2 additions & 2 deletions test/Game.Server.Tests/Services/SurvivorScoreServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ public class SurvivorScoreServiceTests
private readonly Mock<ISurvivorScoreRepository> _mockScoreRepo;
private readonly Mock<IRankingRepository> _mockRankingRepo;
private readonly Mock<IRankingService> _mockRankingService;
private readonly Mock<ISurvivorScoreValidator> _mockScoreValidator;
private readonly Mock<ISurvivorValidator> _mockScoreValidator;
private readonly SurvivorScoreService _service;

public SurvivorScoreServiceTests()
{
_mockScoreRepo = new Mock<ISurvivorScoreRepository>();
_mockRankingRepo = new Mock<IRankingRepository>();
_mockRankingService = new Mock<IRankingService>();
_mockScoreValidator = new Mock<ISurvivorScoreValidator>();
_mockScoreValidator = new Mock<ISurvivorValidator>();

_service = new SurvivorScoreService(
_mockScoreRepo.Object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@

namespace Game.Server.Tests.Validation;

public class SurvivorScoreValidatorTests
public class SurvivorValidatorTests
{
// テストデータ: StageId=1, TimeLimit=120, 3ウェーブ
// Wave 1: ScoreMultiplier=100
// Wave 2: ScoreMultiplier=150
// Wave 3: ScoreMultiplier=200
// Score 上限 (WaveReached=3): 120×(100+150+200) = 54,000

private readonly SurvivorScoreValidator _validator;
private readonly SurvivorValidator _validator;

public SurvivorScoreValidatorTests()
public SurvivorValidatorTests()
{
var resolver = CompositeResolver.Create(MasterMemoryResolver.Instance, StandardResolver.Instance);
var builder = new DatabaseBuilder(resolver);
Expand All @@ -47,9 +47,9 @@ public SurvivorScoreValidatorTests()
var mockMasterData = new Mock<IMasterDataService>();
mockMasterData.Setup(m => m.MemoryDatabase).Returns(db);

_validator = new SurvivorScoreValidator(
_validator = new SurvivorValidator(
mockMasterData.Object,
Mock.Of<ILogger<SurvivorScoreValidator>>());
Mock.Of<ILogger<SurvivorValidator>>());
}

private static ScoreSubmitDto ValidRequest() => new()
Expand All @@ -69,7 +69,7 @@ public void Validate_InvalidStageId_Throws()
var request = ValidRequest();
request.StageId = 999;

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -81,7 +81,7 @@ public void Validate_NegativeScore_Throws()
var request = ValidRequest();
request.Score = -1;

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -93,7 +93,7 @@ public void Validate_NegativeEnemiesDefeated_Throws()
var request = ValidRequest();
request.EnemiesDefeated = -1;

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -105,7 +105,7 @@ public void Validate_ClearTimeExceedsLimit_Throws()
var request = ValidRequest();
request.ClearTime = 126f; // TimeLimit(120) + buffer(5) を超過

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -117,7 +117,7 @@ public void Validate_ZeroClearTime_Throws()
var request = ValidRequest();
request.ClearTime = 0f;

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -127,7 +127,7 @@ public void Validate_NegativeClearTime_Throws()
var request = ValidRequest();
request.ClearTime = -1f;

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -139,7 +139,7 @@ public void Validate_WaveReachedNegative_Throws()
var request = ValidRequest();
request.WaveReached = -1;

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -149,7 +149,7 @@ public void Validate_WaveReachedExceedsMax_Throws()
var request = ValidRequest();
request.WaveReached = 4; // 最大3

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -160,7 +160,7 @@ public void Validate_WaveReachedZero_Passes()
request.WaveReached = 0;
request.Score = 0; // WaveReached=0 なら Score=0 のみ有効

_validator.Validate(request); // 例外なし
_validator.ValidateScoreSubmit(request); // 例外なし
}

[Fact]
Expand All @@ -170,7 +170,7 @@ public void Validate_WaveReachedEqualsMax_Passes()
request.WaveReached = 3;
request.Score = 50000; // 上限 54,000 以内

_validator.Validate(request); // 例外なし
_validator.ValidateScoreSubmit(request); // 例外なし
}

// --- 7. Score 上限 ---
Expand All @@ -182,7 +182,7 @@ public void Validate_ScoreExceedsUpperBound_Throws()
request.WaveReached = 3;
request.Score = 54001; // 上限 54,000 を超過

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -193,7 +193,7 @@ public void Validate_ScoreAtUpperBound_Passes()
request.WaveReached = 3;
request.Score = 54000; // 上限ちょうど: 120×(100+150+200) = 54,000

_validator.Validate(request); // 例外なし
_validator.ValidateScoreSubmit(request); // 例外なし
}

[Fact]
Expand All @@ -203,7 +203,7 @@ public void Validate_WaveReachedZero_OnlyZeroScoreValid()
request.WaveReached = 0;
request.Score = 1; // WaveReached=0 → 上限は 0、Score > 0 は不正

var ex = Assert.Throws<ErrorException>(() => _validator.Validate(request));
var ex = Assert.Throws<ErrorException>(() => _validator.ValidateScoreSubmit(request));
Assert.Equal("INVALID_SCORE", ex.ErrorCode);
}

Expand All @@ -212,6 +212,6 @@ public void Validate_WaveReachedZero_OnlyZeroScoreValid()
[Fact]
public void Validate_AllFieldsValid_Passes()
{
_validator.Validate(ValidRequest()); // 例外なし
_validator.ValidateScoreSubmit(ValidRequest()); // 例外なし
}
}
Loading