Skip to content

Commit 6ed4f19

Browse files
nanotaboadaCopilot
andcommitted
feat(validation): add DateOfBirth rules and fix Birth null mapping (#344)
- Add optional DateOfBirth range validation (past, >= 1900-01-01) in PlayerRequestModelValidator using FluentValidation When(HasValue) - Fix PlayerMappingProfile to return null instead of "" for Birth when DateOfBirth is not provided - Add PlayerValidatorTests covering all validator rule branches - Update PlayerFakes Birth computation to match null-safe mapping - Add TestResults/ to .gitignore (exposed by running coverage suite) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3625020 commit 6ed4f19

5 files changed

Lines changed: 249 additions & 4 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
logs/
22
bin/
33
obj/
4+
TestResults/

src/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ public PlayerMappingProfile()
3737
)
3838
.ForMember(
3939
destination => destination.Birth,
40-
options => options.MapFrom(source => $"{source.DateOfBirth:MMMM d, yyyy}")
40+
options =>
41+
options.MapFrom(source =>
42+
source.DateOfBirth.HasValue
43+
? $"{source.DateOfBirth.Value:MMMM d, yyyy}"
44+
: null
45+
)
4146
)
4247
.ForMember(
4348
destination => destination.Dorsal,

src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ public PlayerRequestModelValidator(IPlayerRepository playerRepository)
3939
.WithMessage("AbbrPosition is required.")
4040
.Must(Position.IsValidAbbr)
4141
.WithMessage("AbbrPosition is invalid.");
42+
43+
When(
44+
player => player.DateOfBirth.HasValue,
45+
() =>
46+
{
47+
RuleFor(player => player.DateOfBirth)
48+
.Must(date => date < DateTime.UtcNow)
49+
.WithMessage("DateOfBirth must be a date in the past.")
50+
.Must(date => date >= new DateTime(1900, 1, 1))
51+
.WithMessage("DateOfBirth must be on or after January 1, 1900.");
52+
}
53+
);
4254
}
4355

4456
private async Task<bool> BeUniqueSquadNumber(
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
using Dotnet.Samples.AspNetCore.WebApi.Models;
2+
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
3+
using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
4+
using Dotnet.Samples.AspNetCore.WebApi.Validators;
5+
using FluentAssertions;
6+
using Moq;
7+
8+
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit;
9+
10+
public class PlayerValidatorTests
11+
{
12+
private static PlayerRequestModelValidator CreateValidator(
13+
Mock<IPlayerRepository>? repositoryMock = null
14+
)
15+
{
16+
var mock = repositoryMock ?? new Mock<IPlayerRepository>();
17+
return new PlayerRequestModelValidator(mock.Object);
18+
}
19+
20+
/* -------------------------------------------------------------------------
21+
* Valid request
22+
* ---------------------------------------------------------------------- */
23+
24+
[Fact]
25+
[Trait("Category", "Unit")]
26+
public async Task GivenValidateAsync_WhenRequestModelIsValid_ThenValidationShouldPass()
27+
{
28+
// Arrange
29+
var request = PlayerFakes.MakeRequestModelForCreate();
30+
var repositoryMock = new Mock<IPlayerRepository>();
31+
repositoryMock
32+
.Setup(r => r.FindBySquadNumberAsync(request.SquadNumber))
33+
.ReturnsAsync(null as Player);
34+
var validator = CreateValidator(repositoryMock);
35+
36+
// Act
37+
var result = await validator.ValidateAsync(request);
38+
39+
// Assert
40+
result.IsValid.Should().BeTrue();
41+
result.Errors.Should().BeEmpty();
42+
}
43+
44+
/* -------------------------------------------------------------------------
45+
* FirstName
46+
* ---------------------------------------------------------------------- */
47+
48+
[Fact]
49+
[Trait("Category", "Unit")]
50+
public async Task GivenValidateAsync_WhenFirstNameIsEmpty_ThenValidationShouldFail()
51+
{
52+
// Arrange
53+
var request = PlayerFakes.MakeRequestModelForCreate();
54+
request.FirstName = string.Empty;
55+
var validator = CreateValidator();
56+
57+
// Act
58+
var result = await validator.ValidateAsync(request);
59+
60+
// Assert
61+
result.IsValid.Should().BeFalse();
62+
result.Errors.Should().Contain(e => e.PropertyName == "FirstName");
63+
}
64+
65+
/* -------------------------------------------------------------------------
66+
* LastName
67+
* ---------------------------------------------------------------------- */
68+
69+
[Fact]
70+
[Trait("Category", "Unit")]
71+
public async Task GivenValidateAsync_WhenLastNameIsEmpty_ThenValidationShouldFail()
72+
{
73+
// Arrange
74+
var request = PlayerFakes.MakeRequestModelForCreate();
75+
request.LastName = string.Empty;
76+
var validator = CreateValidator();
77+
78+
// Act
79+
var result = await validator.ValidateAsync(request);
80+
81+
// Assert
82+
result.IsValid.Should().BeFalse();
83+
result.Errors.Should().Contain(e => e.PropertyName == "LastName");
84+
}
85+
86+
/* -------------------------------------------------------------------------
87+
* SquadNumber
88+
* ---------------------------------------------------------------------- */
89+
90+
[Fact]
91+
[Trait("Category", "Unit")]
92+
public async Task GivenValidateAsync_WhenSquadNumberIsNotGreaterThanZero_ThenValidationShouldFail()
93+
{
94+
// Arrange
95+
var request = PlayerFakes.MakeRequestModelForCreate();
96+
request.SquadNumber = 0;
97+
var validator = CreateValidator();
98+
99+
// Act
100+
var result = await validator.ValidateAsync(request);
101+
102+
// Assert
103+
result.IsValid.Should().BeFalse();
104+
result.Errors.Should().Contain(e => e.PropertyName == "SquadNumber");
105+
}
106+
107+
[Fact]
108+
[Trait("Category", "Unit")]
109+
public async Task GivenValidateAsync_WhenSquadNumberIsNotUnique_ThenValidationShouldFail()
110+
{
111+
// Arrange
112+
var request = PlayerFakes.MakeRequestModelForCreate();
113+
var existingPlayer = PlayerFakes.MakeNew();
114+
var repositoryMock = new Mock<IPlayerRepository>();
115+
repositoryMock
116+
.Setup(r => r.FindBySquadNumberAsync(request.SquadNumber))
117+
.ReturnsAsync(existingPlayer);
118+
var validator = CreateValidator(repositoryMock);
119+
120+
// Act
121+
var result = await validator.ValidateAsync(request);
122+
123+
// Assert
124+
result.IsValid.Should().BeFalse();
125+
result
126+
.Errors.Should()
127+
.Contain(e => e.PropertyName == "SquadNumber" && e.ErrorMessage.Contains("unique"));
128+
}
129+
130+
/* -------------------------------------------------------------------------
131+
* AbbrPosition
132+
* ---------------------------------------------------------------------- */
133+
134+
[Fact]
135+
[Trait("Category", "Unit")]
136+
public async Task GivenValidateAsync_WhenAbbrPositionIsEmpty_ThenValidationShouldFail()
137+
{
138+
// Arrange
139+
var request = PlayerFakes.MakeRequestModelForCreate();
140+
request.AbbrPosition = string.Empty;
141+
var validator = CreateValidator();
142+
143+
// Act
144+
var result = await validator.ValidateAsync(request);
145+
146+
// Assert
147+
result.IsValid.Should().BeFalse();
148+
result.Errors.Should().Contain(e => e.PropertyName == "AbbrPosition");
149+
}
150+
151+
[Fact]
152+
[Trait("Category", "Unit")]
153+
public async Task GivenValidateAsync_WhenAbbrPositionIsInvalid_ThenValidationShouldFail()
154+
{
155+
// Arrange
156+
var request = PlayerFakes.MakeRequestModelForCreate();
157+
request.AbbrPosition = "INVALID";
158+
var validator = CreateValidator();
159+
160+
// Act
161+
var result = await validator.ValidateAsync(request);
162+
163+
// Assert
164+
result.IsValid.Should().BeFalse();
165+
result.Errors.Should().Contain(e => e.PropertyName == "AbbrPosition");
166+
}
167+
168+
/* -------------------------------------------------------------------------
169+
* DateOfBirth
170+
* ---------------------------------------------------------------------- */
171+
172+
[Fact]
173+
[Trait("Category", "Unit")]
174+
public async Task GivenValidateAsync_WhenDateOfBirthIsNull_ThenValidationShouldPass()
175+
{
176+
// Arrange
177+
var request = PlayerFakes.MakeRequestModelForCreate();
178+
request.DateOfBirth = null;
179+
var repositoryMock = new Mock<IPlayerRepository>();
180+
repositoryMock
181+
.Setup(r => r.FindBySquadNumberAsync(request.SquadNumber))
182+
.ReturnsAsync(null as Player);
183+
var validator = CreateValidator(repositoryMock);
184+
185+
// Act
186+
var result = await validator.ValidateAsync(request);
187+
188+
// Assert
189+
result.IsValid.Should().BeTrue();
190+
}
191+
192+
[Fact]
193+
[Trait("Category", "Unit")]
194+
public async Task GivenValidateAsync_WhenDateOfBirthIsInTheFuture_ThenValidationShouldFail()
195+
{
196+
// Arrange
197+
var request = PlayerFakes.MakeRequestModelForCreate();
198+
request.DateOfBirth = DateTime.UtcNow.AddYears(1);
199+
var validator = CreateValidator();
200+
201+
// Act
202+
var result = await validator.ValidateAsync(request);
203+
204+
// Assert
205+
result.IsValid.Should().BeFalse();
206+
result.Errors.Should().Contain(e => e.PropertyName == "DateOfBirth");
207+
}
208+
209+
[Fact]
210+
[Trait("Category", "Unit")]
211+
public async Task GivenValidateAsync_WhenDateOfBirthIsBeforeYear1900_ThenValidationShouldFail()
212+
{
213+
// Arrange
214+
var request = PlayerFakes.MakeRequestModelForCreate();
215+
request.DateOfBirth = new DateTime(1899, 12, 31);
216+
var validator = CreateValidator();
217+
218+
// Act
219+
var result = await validator.ValidateAsync(request);
220+
221+
// Assert
222+
result.IsValid.Should().BeFalse();
223+
result.Errors.Should().Contain(e => e.PropertyName == "DateOfBirth");
224+
}
225+
}

test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public static PlayerResponseModel MakeResponseModelForCreate()
101101
{
102102
FullName =
103103
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
104-
Birth = $"{player.DateOfBirth:MMMM d, yyyy}",
104+
Birth = player.DateOfBirth.HasValue ? $"{player.DateOfBirth.Value:MMMM d, yyyy}" : null,
105105
Dorsal = player.SquadNumber,
106106
Position = player.Position,
107107
Club = player.Team,
@@ -157,7 +157,7 @@ public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber)
157157
{
158158
FullName =
159159
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
160-
Birth = $"{player.DateOfBirth:MMMM d, yyyy}",
160+
Birth = player.DateOfBirth.HasValue ? $"{player.DateOfBirth.Value:MMMM d, yyyy}" : null,
161161
Dorsal = player.SquadNumber,
162162
Position = player.Position,
163163
Club = player.Team,
@@ -181,7 +181,9 @@ .. PlayerData
181181
{
182182
FullName =
183183
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
184-
Birth = $"{player.DateOfBirth:MMMM d, yyyy}",
184+
Birth = player.DateOfBirth.HasValue
185+
? $"{player.DateOfBirth.Value:MMMM d, yyyy}"
186+
: null,
185187
Dorsal = player.SquadNumber,
186188
Position = player.Position,
187189
Club = player.Team,

0 commit comments

Comments
 (0)