Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
logs/
bin/
obj/
TestResults/
29 changes: 29 additions & 0 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"servers": {
// GitHub MCP Server - Interact with GitHub APIs (issues, PRs, repos, etc.)
// https://github.com/github/github-mcp-server
"github": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
}
}
},
"inputs": [
{
"id": "github_token",
"type": "promptString",
"description": "GitHub Personal Access Token. Prefer fine-grained PATs with minimum permissions: Contents (read), Issues (read & write), Pull requests (read & write). If a classic PAT is unavoidable, minimum scope: repo.",
"password": true
Comment thread
nanotaboada marked this conversation as resolved.
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ public PlayerMappingProfile()
)
.ForMember(
destination => destination.Birth,
options => options.MapFrom(source => $"{source.DateOfBirth:MMMM d, yyyy}")
options =>
options.MapFrom(source =>
source.DateOfBirth.HasValue
? $"{source.DateOfBirth.Value:MMMM d, yyyy}"
: null
)
)
.ForMember(
destination => destination.Dorsal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ public PlayerRequestModelValidator(IPlayerRepository playerRepository)
.WithMessage("AbbrPosition is required.")
.Must(Position.IsValidAbbr)
.WithMessage("AbbrPosition is invalid.");

When(
player => player.DateOfBirth.HasValue,
() =>
{
RuleFor(player => player.DateOfBirth)
.Must(date => date < DateTime.UtcNow)
.WithMessage("DateOfBirth must be a date in the past.")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.Must(date => date >= new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc))
.WithMessage("DateOfBirth must be on or after January 1, 1900.");
}
);
}

private async Task<bool> BeUniqueSquadNumber(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using Dotnet.Samples.AspNetCore.WebApi.Models;
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
using Dotnet.Samples.AspNetCore.WebApi.Validators;
using FluentAssertions;
using Moq;

namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit;

public class PlayerValidatorTests
{
private static PlayerRequestModelValidator CreateValidator(
Mock<IPlayerRepository>? repositoryMock = null
)
{
var mock = repositoryMock ?? new Mock<IPlayerRepository>();
return new PlayerRequestModelValidator(mock.Object);
}

/* -------------------------------------------------------------------------
* Valid request
* ---------------------------------------------------------------------- */

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenRequestModelIsValid_ThenValidationShouldPass()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
var repositoryMock = new Mock<IPlayerRepository>();
repositoryMock
.Setup(r => r.FindBySquadNumberAsync(request.SquadNumber))
.ReturnsAsync(null as Player);
var validator = CreateValidator(repositoryMock);

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeTrue();
result.Errors.Should().BeEmpty();
}

/* -------------------------------------------------------------------------
* FirstName
* ---------------------------------------------------------------------- */

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenFirstNameIsEmpty_ThenValidationShouldFail()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
request.FirstName = string.Empty;
var validator = CreateValidator();

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "FirstName");
}

/* -------------------------------------------------------------------------
* LastName
* ---------------------------------------------------------------------- */

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenLastNameIsEmpty_ThenValidationShouldFail()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
request.LastName = string.Empty;
var validator = CreateValidator();

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "LastName");
}

/* -------------------------------------------------------------------------
* SquadNumber
* ---------------------------------------------------------------------- */

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenSquadNumberIsNotGreaterThanZero_ThenValidationShouldFail()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
request.SquadNumber = 0;
var validator = CreateValidator();

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "SquadNumber");
}

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenSquadNumberIsNotUnique_ThenValidationShouldFail()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
var existingPlayer = PlayerFakes.MakeNew();
var repositoryMock = new Mock<IPlayerRepository>();
repositoryMock
.Setup(r => r.FindBySquadNumberAsync(request.SquadNumber))
.ReturnsAsync(existingPlayer);
var validator = CreateValidator(repositoryMock);

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();
result
.Errors.Should()
.Contain(e => e.PropertyName == "SquadNumber" && e.ErrorMessage.Contains("unique"));
}

/* -------------------------------------------------------------------------
* AbbrPosition
* ---------------------------------------------------------------------- */

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenAbbrPositionIsEmpty_ThenValidationShouldFail()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
request.AbbrPosition = string.Empty;
var validator = CreateValidator();

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "AbbrPosition");
}

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenAbbrPositionIsInvalid_ThenValidationShouldFail()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
request.AbbrPosition = "INVALID";
var validator = CreateValidator();

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "AbbrPosition");
}

/* -------------------------------------------------------------------------
* DateOfBirth
* ---------------------------------------------------------------------- */

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenDateOfBirthIsNull_ThenValidationShouldPass()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
request.DateOfBirth = null;
var repositoryMock = new Mock<IPlayerRepository>();
repositoryMock
.Setup(r => r.FindBySquadNumberAsync(request.SquadNumber))
.ReturnsAsync(null as Player);
var validator = CreateValidator(repositoryMock);

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeTrue();
}

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenDateOfBirthIsInTheFuture_ThenValidationShouldFail()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
request.DateOfBirth = DateTime.UtcNow.AddYears(1);
var validator = CreateValidator();

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "DateOfBirth");
}

[Fact]
[Trait("Category", "Unit")]
public async Task GivenValidateAsync_WhenDateOfBirthIsBeforeYear1900_ThenValidationShouldFail()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
request.DateOfBirth = new DateTime(1899, 12, 31, 0, 0, 0, DateTimeKind.Utc);
var validator = CreateValidator();

// Act
var result = await validator.ValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "DateOfBirth");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
/// </summary>
public static class PlayerFakes
{
private static string? FormatBirth(DateTime? dateOfBirth) =>
dateOfBirth.HasValue ? $"{dateOfBirth.Value:MMMM d, yyyy}" : null;

/// <summary>
/// Returns the starting 11 players with generated GUIDs for in-memory testing.
/// Reuses production player data from PlayerData.MakeStarting11().
Expand Down Expand Up @@ -101,7 +104,7 @@ public static PlayerResponseModel MakeResponseModelForCreate()
{
FullName =
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
Birth = $"{player.DateOfBirth:MMMM d, yyyy}",
Birth = FormatBirth(player.DateOfBirth),
Dorsal = player.SquadNumber,
Position = player.Position,
Club = player.Team,
Expand Down Expand Up @@ -157,7 +160,7 @@ public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber)
{
FullName =
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
Birth = $"{player.DateOfBirth:MMMM d, yyyy}",
Birth = FormatBirth(player.DateOfBirth),
Dorsal = player.SquadNumber,
Position = player.Position,
Club = player.Team,
Expand All @@ -181,7 +184,7 @@ .. PlayerData
{
FullName =
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
Birth = $"{player.DateOfBirth:MMMM d, yyyy}",
Birth = FormatBirth(player.DateOfBirth),
Dorsal = player.SquadNumber,
Position = player.Position,
Club = player.Team,
Expand Down