Skip to content

Commit a235423

Browse files
♻️ Add unit tests to core functionality (#1469)
* Add unit tests and fix mapping * Update src/Application/Achievements/Queries/Common/Mapping.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Application/Users/Queries/GetUser/Mapping.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Application/Users/Queries/GetCurrentUser/Mapping.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Application/Staff/Queries/GetStaffList/Mapping.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Application/Rewards/Common/Mapping.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Application/Quizzes/Common/Mapping.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 57d79e2 commit a235423

File tree

15 files changed

+2077
-6
lines changed

15 files changed

+2077
-6
lines changed

src/Application/Achievements/Queries/Common/Mapping.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ public class Mapping : Profile
55
{
66
public Mapping()
77
{
8-
CreateMap<Achievement, AchievementDto>();
8+
CreateMap<Achievement, AchievementDto>()
9+
.ForMember(dst => dst.UserId, opt => opt.Ignore());
910

1011
CreateMap<UserAchievement, AchievementUserDto>()
1112
.ForMember(dst => dst.UserName, opt => opt.MapFrom(src => src.User.FullName))

src/Application/Quizzes/Common/Mapping.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@ public Mapping()
1010
.ForMember(dst => dst.DateCreated, opt => opt.MapFrom(src => src.CreatedUtc))
1111
.ForMember(dst => dst.Points, opt => opt.MapFrom(src => src.Achievement.Value))
1212
.ForMember(dst => dst.ThumbnailImage, opt => opt.MapFrom(src => src.ThumbnailImage))
13-
.ForMember(dst => dst.CarouselImage, opt => opt.MapFrom(src => src.CarouselImage));
13+
.ForMember(dst => dst.CarouselImage, opt => opt.MapFrom(src => src.CarouselImage))
14+
.ForMember(dst => dst.NotifyUsers, opt => opt.Ignore());
1415

1516
CreateMap<Quiz, QuizDetailsDto>()
1617
.ForMember(dst => dst.QuizId, opt => opt.MapFrom(src => src.Id))
1718
.ForMember(dst => dst.DateCreated, opt => opt.MapFrom(src => src.CreatedUtc))
1819
.ForMember(dst => dst.Points, opt => opt.MapFrom(src => src.Achievement.Value));
1920

2021
CreateMap<QuizQuestion, QuizQuestionDto>()
21-
.ForMember(dst => dst.QuestionId, opt => opt.MapFrom(src => src.Id));
22+
.ForMember(dst => dst.QuestionId, opt => opt.MapFrom(src => src.Id))
23+
.ForMember(dst => dst.Answer, opt => opt.Ignore());
2224

2325
CreateMap<QuizAnswer, QuestionAnswerDto>()
2426
.ForMember(dst => dst.QuestionAnswerId, opt => opt.MapFrom(src => src.Id));
2527

2628
CreateMap<QuizQuestion, QuizQuestionEditDto>()
27-
.ForMember(dst => dst.QuestionId, opt => opt.MapFrom(src => src.Id));
29+
.ForMember(dst => dst.QuestionId, opt => opt.MapFrom(src => src.Id))
30+
.ForMember(dst => dst.IsDeleted, opt => opt.Ignore());
2831

2932
CreateMap<QuizAnswer, QuestionAnswerEditDto>()
3033
.ForMember(dst => dst.QuestionAnswerId, opt => opt.MapFrom(src => src.Id));

src/Application/Rewards/Common/Mapping.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ public class Mapping : Profile
66
public Mapping()
77
{
88
CreateMap<Reward, RewardDto>();
9-
CreateMap<Reward, RewardEditDto>();
9+
CreateMap<Reward, RewardEditDto>()
10+
.ForMember(dst => dst.ImageBytesInBase64, opt => opt.Ignore())
11+
.ForMember(dst => dst.ImageFileName, opt => opt.Ignore())
12+
.ForMember(dst => dst.CarouselImageBytesInBase64, opt => opt.Ignore())
13+
.ForMember(dst => dst.CarouselImageFileName, opt => opt.Ignore())
14+
.ForMember(dst => dst.DeleteThumbnailImage, opt => opt.Ignore())
15+
.ForMember(dst => dst.DeleteCarouselImage, opt => opt.Ignore());
1016
}
1117
}

src/Application/Staff/Queries/GetStaffList/Mapping.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public Mapping()
1313

1414
CreateMap<StaffMember, StaffMemberDto>()
1515
.ForMember(dest => dest.Skills, opt => opt.MapFrom(src => src.StaffMemberSkills))
16+
.ForMember(dest => dest.Points, opt => opt.Ignore())
1617
.ForMember(dest => dest.Scanned, opt => opt.Ignore())
1718
.ForMember(dest => dest.IsDeleted, opt => opt.MapFrom(src => src.DeletedUtc != null));
1819
}

src/Application/Users/Queries/GetCurrentUser/Mapping.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public Mapping()
1010
.ForMember(dst => dst.Points, opt => opt.MapFrom(src => src.UserAchievements.Sum(ua => ua.Achievement.Value)))
1111
.ForMember(dst => dst.Balance, opt => opt.MapFrom(src => src.UserAchievements.Sum(ua => ua.Achievement.Value) - src.UserRewards.Sum(ur => ur.Reward.Cost)))
1212
.ForMember(dst => dst.ProfilePic, opt => opt.MapFrom(src => src.Avatar))
13-
.ForMember(dst => dst.QRCode, opt => opt.MapFrom((src, dst) => src?.Achievement?.Code?? string.Empty));
13+
.ForMember(dst => dst.QRCode, opt => opt.MapFrom((src, dst) => src?.Achievement?.Code?? string.Empty))
14+
.ForMember(dst => dst.IsStaff, opt => opt.Ignore())
15+
.ForMember(dst => dst.Rank, opt => opt.Ignore());
1416
}
1517
}

src/Application/Users/Queries/GetUser/Mapping.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public Mapping()
99
.ForMember(dst => dst.ProfilePic, opt => opt.MapFrom(src => src.Avatar))
1010
.ForMember(dst => dst.Points, opt => opt.MapFrom(src => src.UserAchievements.Sum(ua => ua.Achievement.Value)))
1111
.ForMember(dst => dst.Balance, opt => opt.Ignore())
12+
.ForMember(dst => dst.IsStaff, opt => opt.Ignore())
13+
.ForMember(dst => dst.Rank, opt => opt.Ignore())
1214
.ForMember(dst => dst.Rewards, opt => opt.MapFrom(src => src.UserRewards))
1315
.ForMember(dst => dst.Achievements, opt => opt.MapFrom(src => src.UserAchievements));
1416
}
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
using FluentAssertions;
2+
using MediatR;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Logging;
5+
using MockQueryable.Moq;
6+
using Moq;
7+
using NUnit.Framework;
8+
using SSW.Rewards.Application.Achievements.Command.ClaimAchievementForUser;
9+
using SSW.Rewards.Application.Achievements.Notifications;
10+
using SSW.Rewards.Application.Common.Interfaces;
11+
using SSW.Rewards.Domain.Entities;
12+
using SSW.Rewards.Enums;
13+
using SSW.Rewards.Shared.DTOs.Achievements;
14+
15+
namespace SSW.Rewards.Application.UnitTests.Achievements.Commands;
16+
17+
/// <summary>
18+
/// Unit tests for ClaimAchievementForUserCommand - handles achievement claiming for users.
19+
/// Tests achievement validation, duplicate detection, user validation, and milestone triggers.
20+
/// </summary>
21+
[TestFixture]
22+
public class ClaimAchievementForUserCommandTests
23+
{
24+
private Mock<IApplicationDbContext> _contextMock = null!;
25+
private Mock<ILogger<ClaimAchievementForUserCommand>> _loggerMock = null!;
26+
private Mock<IMediator> _mediatorMock = null!;
27+
private ClaimAchievementForUserCommandHandler _handler = null!;
28+
29+
[SetUp]
30+
public void Setup()
31+
{
32+
_contextMock = new Mock<IApplicationDbContext>();
33+
_loggerMock = new Mock<ILogger<ClaimAchievementForUserCommand>>();
34+
_mediatorMock = new Mock<IMediator>();
35+
36+
_handler = new ClaimAchievementForUserCommandHandler(
37+
_contextMock.Object,
38+
_loggerMock.Object,
39+
_mediatorMock.Object);
40+
}
41+
42+
[Test]
43+
public async Task Handle_WithValidAchievement_ShouldClaimSuccessfully()
44+
{
45+
// Arrange
46+
const int userId = 1;
47+
const int achievementId = 10;
48+
const string achievementCode = "TEST_ACHIEVEMENT";
49+
50+
SetupAchievementQuery(achievementId, achievementCode);
51+
SetupUserQuery(userId);
52+
53+
var capturedAchievements = new List<UserAchievement>();
54+
var mockUserAchievements = new List<UserAchievement>().AsQueryable().BuildMockDbSet();
55+
mockUserAchievements.Setup(x => x.Add(It.IsAny<UserAchievement>()))
56+
.Callback<UserAchievement>(capturedAchievements.Add);
57+
58+
_contextMock.Setup(x => x.UserAchievements).Returns(mockUserAchievements.Object);
59+
_contextMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
60+
.ReturnsAsync(1);
61+
62+
var command = new ClaimAchievementForUserCommand
63+
{
64+
UserId = userId,
65+
Code = achievementCode
66+
};
67+
68+
// Act
69+
var result = await _handler.Handle(command, CancellationToken.None);
70+
71+
// Assert
72+
result.Should().NotBeNull();
73+
result.status.Should().Be(ClaimAchievementStatus.Claimed);
74+
75+
capturedAchievements.Should().HaveCount(1);
76+
capturedAchievements[0].UserId.Should().Be(userId);
77+
capturedAchievements[0].AchievementId.Should().Be(achievementId);
78+
79+
_mediatorMock.Verify(x => x.Publish(
80+
It.Is<UserMilestoneAchievementCheckRequested>(n => n.UserId == userId),
81+
It.IsAny<CancellationToken>()), Times.Once);
82+
}
83+
84+
[Test]
85+
public async Task Handle_WithNonExistentAchievement_ShouldReturnNotFound()
86+
{
87+
// Arrange
88+
const int userId = 1;
89+
const string nonExistentCode = "INVALID_CODE";
90+
91+
SetupAchievementQuery(null, nonExistentCode);
92+
93+
var command = new ClaimAchievementForUserCommand
94+
{
95+
UserId = userId,
96+
Code = nonExistentCode
97+
};
98+
99+
// Act
100+
var result = await _handler.Handle(command, CancellationToken.None);
101+
102+
// Assert
103+
result.Should().NotBeNull();
104+
result.status.Should().Be(ClaimAchievementStatus.NotFound);
105+
106+
_contextMock.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Never);
107+
_mediatorMock.Verify(x => x.Publish(It.IsAny<INotification>(), It.IsAny<CancellationToken>()), Times.Never);
108+
}
109+
110+
[Test]
111+
public async Task Handle_WithNonExistentUser_ShouldReturnError()
112+
{
113+
// Arrange
114+
const int nonExistentUserId = 999;
115+
const string achievementCode = "TEST_ACHIEVEMENT";
116+
117+
SetupAchievementQuery(10, achievementCode);
118+
SetupUserQuery(null);
119+
120+
var command = new ClaimAchievementForUserCommand
121+
{
122+
UserId = nonExistentUserId,
123+
Code = achievementCode
124+
};
125+
126+
// Act
127+
var result = await _handler.Handle(command, CancellationToken.None);
128+
129+
// Assert
130+
result.Should().NotBeNull();
131+
result.status.Should().Be(ClaimAchievementStatus.Error);
132+
133+
_contextMock.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Never);
134+
}
135+
136+
[Test]
137+
public async Task Handle_WithDuplicateAchievement_ShouldReturnDuplicate()
138+
{
139+
// Arrange
140+
const int userId = 1;
141+
const int achievementId = 10;
142+
const string achievementCode = "TEST_ACHIEVEMENT";
143+
144+
var existingAchievement = new UserAchievement
145+
{
146+
UserId = userId,
147+
AchievementId = achievementId,
148+
Achievement = new Achievement { Code = achievementCode }
149+
};
150+
151+
SetupAchievementQuery(achievementId, achievementCode);
152+
SetupUserQuery(userId);
153+
SetupUserAchievementsQuery(new List<UserAchievement> { existingAchievement });
154+
155+
var command = new ClaimAchievementForUserCommand
156+
{
157+
UserId = userId,
158+
Code = achievementCode
159+
};
160+
161+
// Act
162+
var result = await _handler.Handle(command, CancellationToken.None);
163+
164+
// Assert
165+
result.Should().NotBeNull();
166+
result.status.Should().Be(ClaimAchievementStatus.Duplicate);
167+
168+
_contextMock.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Never);
169+
_mediatorMock.Verify(x => x.Publish(It.IsAny<INotification>(), It.IsAny<CancellationToken>()), Times.Never);
170+
}
171+
172+
[Test]
173+
public async Task Handle_WhenSaveFails_ShouldReturnError()
174+
{
175+
// Arrange
176+
const int userId = 1;
177+
const int achievementId = 10;
178+
const string achievementCode = "TEST_ACHIEVEMENT";
179+
180+
SetupAchievementQuery(achievementId, achievementCode);
181+
SetupUserQuery(userId);
182+
183+
var mockUserAchievements = new List<UserAchievement>().AsQueryable().BuildMockDbSet();
184+
_contextMock.Setup(x => x.UserAchievements).Returns(mockUserAchievements.Object);
185+
186+
_contextMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
187+
.ThrowsAsync(new Exception("Database error"));
188+
189+
var command = new ClaimAchievementForUserCommand
190+
{
191+
UserId = userId,
192+
Code = achievementCode
193+
};
194+
195+
// Act
196+
var result = await _handler.Handle(command, CancellationToken.None);
197+
198+
// Assert
199+
result.Should().NotBeNull();
200+
result.status.Should().Be(ClaimAchievementStatus.Error);
201+
202+
_mediatorMock.Verify(x => x.Publish(It.IsAny<INotification>(), It.IsAny<CancellationToken>()), Times.Never);
203+
}
204+
205+
[Test]
206+
public async Task Handle_OnSuccess_ShouldPublishMilestoneCheckNotification()
207+
{
208+
// Arrange
209+
const int userId = 5;
210+
const int achievementId = 15;
211+
const string achievementCode = "MILESTONE_TRIGGER";
212+
213+
SetupAchievementQuery(achievementId, achievementCode);
214+
SetupUserQuery(userId);
215+
216+
var mockUserAchievements = new List<UserAchievement>().AsQueryable().BuildMockDbSet();
217+
_contextMock.Setup(x => x.UserAchievements).Returns(mockUserAchievements.Object);
218+
_contextMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
219+
.ReturnsAsync(1);
220+
221+
UserMilestoneAchievementCheckRequested? capturedNotification = null;
222+
_mediatorMock.Setup(x => x.Publish(It.IsAny<UserMilestoneAchievementCheckRequested>(), It.IsAny<CancellationToken>()))
223+
.Callback<INotification, CancellationToken>((n, ct) => capturedNotification = n as UserMilestoneAchievementCheckRequested)
224+
.Returns(Task.CompletedTask);
225+
226+
var command = new ClaimAchievementForUserCommand
227+
{
228+
UserId = userId,
229+
Code = achievementCode
230+
};
231+
232+
// Act
233+
await _handler.Handle(command, CancellationToken.None);
234+
235+
// Assert
236+
capturedNotification.Should().NotBeNull();
237+
capturedNotification!.UserId.Should().Be(userId);
238+
}
239+
240+
[Test]
241+
public async Task Handle_WithMultipleDifferentUsers_ShouldClaimForEach()
242+
{
243+
// Arrange
244+
const string achievementCode = "SHARED_ACHIEVEMENT";
245+
const int achievementId = 20;
246+
247+
var capturedAchievements = new List<UserAchievement>();
248+
var mockUserAchievements = new List<UserAchievement>().AsQueryable().BuildMockDbSet();
249+
mockUserAchievements.Setup(x => x.Add(It.IsAny<UserAchievement>()))
250+
.Callback<UserAchievement>(capturedAchievements.Add);
251+
252+
_contextMock.Setup(x => x.UserAchievements).Returns(mockUserAchievements.Object);
253+
_contextMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
254+
.ReturnsAsync(1);
255+
256+
// User 1
257+
SetupAchievementQuery(achievementId, achievementCode);
258+
SetupUserQuery(1);
259+
260+
var command1 = new ClaimAchievementForUserCommand { UserId = 1, Code = achievementCode };
261+
await _handler.Handle(command1, CancellationToken.None);
262+
263+
// User 2
264+
SetupAchievementQuery(achievementId, achievementCode);
265+
SetupUserQuery(2);
266+
267+
var command2 = new ClaimAchievementForUserCommand { UserId = 2, Code = achievementCode };
268+
await _handler.Handle(command2, CancellationToken.None);
269+
270+
// Assert
271+
capturedAchievements.Should().HaveCount(2);
272+
capturedAchievements[0].UserId.Should().Be(1);
273+
capturedAchievements[1].UserId.Should().Be(2);
274+
}
275+
276+
// Helper methods
277+
private void SetupAchievementQuery(int? achievementId, string code)
278+
{
279+
var achievements = achievementId.HasValue
280+
? new List<Achievement> { new() { Id = achievementId.Value, Code = code } }
281+
: new List<Achievement>();
282+
283+
var mockDbSet = achievements.AsQueryable().BuildMockDbSet();
284+
_contextMock.Setup(x => x.Achievements).Returns(mockDbSet.Object);
285+
}
286+
287+
private void SetupUserQuery(int? userId)
288+
{
289+
var users = userId.HasValue
290+
? new List<User> { new() { Id = userId.Value } }
291+
: new List<User>();
292+
293+
var mockDbSet = users.AsQueryable().BuildMockDbSet();
294+
_contextMock.Setup(x => x.Users).Returns(mockDbSet.Object);
295+
}
296+
297+
private void SetupUserAchievementsQuery(List<UserAchievement> userAchievements)
298+
{
299+
var mockDbSet = userAchievements.AsQueryable().BuildMockDbSet();
300+
_contextMock.Setup(x => x.UserAchievements).Returns(mockDbSet.Object);
301+
}
302+
}
303+

tests/Application.UnitTests/Application.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
</PackageReference>
2020
<PackageReference Include="FluentAssertions" Version="6.12.0" />
2121
<PackageReference Include="Moq" Version="4.20.70" />
22+
<PackageReference Include="MockQueryable.Moq" Version="7.0.3" />
2223
</ItemGroup>
2324

2425
<ItemGroup>

0 commit comments

Comments
 (0)