Skip to content

Commit 25f3f83

Browse files
committed
Test - migrate all tests to factory methods, add missing coverage
Migrate 15 test files from new Entity {} initializers to domain factory methods and behavior methods. Add SetPrivateField/SetField reflection helpers for navigation properties. Add 21 new tests: 11 mapper tests, 6 mailbox query handler tests (GetUnreadPaged, GetTrashPaged, GetStarredPaged, GetSpamPaged, GetByThreadPaged, GetByLabelPaged), 4 pipeline behavior tests (LoggingBehavior, TransactionBehavior). Fix integration test pagination assertion (expect pageSize+1). Fix pre-existing DeleteLabel_WrongOwner assertion bug. Add ThreadEntity using alias to resolve System.Threading ambiguity.
1 parent 2c3b387 commit 25f3f83

30 files changed

Lines changed: 895 additions & 334 deletions
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using MailCore.Application.Common.Behaviors;
2+
using MediatR;
3+
using Microsoft.Extensions.Logging;
4+
using Moq;
5+
6+
namespace MailCore.Application.Tests.Behaviors;
7+
8+
public class LoggingBehaviorTests
9+
{
10+
public record TestRequest(string Value) : IRequest<string>;
11+
12+
private static Task<string> SuccessNext() => Task.FromResult("ok");
13+
private static Task<string> FailureNext() => throw new InvalidOperationException("fail");
14+
15+
[Fact]
16+
public async Task Handle_SuccessfulRequest_LogsStartAndEnd()
17+
{
18+
var loggerMock = new Mock<ILogger<LoggingBehavior<TestRequest, string>>>();
19+
var behavior = new LoggingBehavior<TestRequest, string>(loggerMock.Object);
20+
21+
var result = await behavior.Handle(new TestRequest("x"), _ => SuccessNext(), default);
22+
23+
Assert.Equal("ok", result);
24+
loggerMock.Verify(
25+
x => x.Log(
26+
LogLevel.Information,
27+
It.IsAny<EventId>(),
28+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Handling")),
29+
It.IsAny<Exception>(),
30+
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
31+
Times.Once);
32+
loggerMock.Verify(
33+
x => x.Log(
34+
LogLevel.Information,
35+
It.IsAny<EventId>(),
36+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Handled")),
37+
It.IsAny<Exception>(),
38+
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
39+
Times.Once);
40+
}
41+
42+
[Fact]
43+
public async Task Handle_FailingRequest_LogsStartAndFailureAndPropagates()
44+
{
45+
var loggerMock = new Mock<ILogger<LoggingBehavior<TestRequest, string>>>();
46+
var behavior = new LoggingBehavior<TestRequest, string>(loggerMock.Object);
47+
48+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
49+
() => behavior.Handle(new TestRequest("x"), _ => FailureNext(), default));
50+
51+
Assert.Equal("fail", ex.Message);
52+
loggerMock.Verify(
53+
x => x.Log(
54+
LogLevel.Information,
55+
It.IsAny<EventId>(),
56+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Handling")),
57+
It.IsAny<Exception>(),
58+
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
59+
Times.Once);
60+
loggerMock.Verify(
61+
x => x.Log(
62+
LogLevel.Warning,
63+
It.IsAny<EventId>(),
64+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Failed")),
65+
It.IsAny<Exception>(),
66+
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
67+
Times.Once);
68+
}
69+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using MailCore.Application.Common.Behaviors;
2+
using MailCore.Domain.Common;
3+
using MailCore.Domain.Interfaces;
4+
using MediatR;
5+
using Moq;
6+
7+
namespace MailCore.Application.Tests.Behaviors;
8+
9+
public class TransactionBehaviorTests
10+
{
11+
private record TestCommand(string Value) : IRequest<string>, ICommand;
12+
13+
private static Task<string> SuccessNext() => Task.FromResult("ok");
14+
private static Task<string> FailureNext() => throw new InvalidOperationException("fail");
15+
16+
[Fact]
17+
public async Task Handle_SuccessfulRequest_CallsSaveChangesOnce()
18+
{
19+
var uowMock = new Mock<IUnitOfWork>();
20+
var behavior = new TransactionBehavior<TestCommand, string>(uowMock.Object);
21+
22+
var result = await behavior.Handle(new TestCommand("x"), _ => SuccessNext(), default);
23+
24+
Assert.Equal("ok", result);
25+
uowMock.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
26+
}
27+
28+
[Fact]
29+
public async Task Handle_FailingRequest_DoesNotCallSaveChanges()
30+
{
31+
var uowMock = new Mock<IUnitOfWork>();
32+
var behavior = new TransactionBehavior<TestCommand, string>(uowMock.Object);
33+
34+
await Assert.ThrowsAsync<InvalidOperationException>(
35+
() => behavior.Handle(new TestCommand("x"), _ => FailureNext(), default));
36+
37+
uowMock.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Never);
38+
}
39+
}

MailCore.Application.Tests/Commands/DraftCommandHandlerTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ await handler.Handle(
5555
[Fact]
5656
public async Task UpdateDraft_ExistingOwnedDraft_UpdatesAndReturnsTrue()
5757
{
58-
var draft = new Draft { Id = _draftId, UserId = _userId, Subject = "Old", Body = "Old body" };
58+
var draft = Draft.Create(_userId, "Old", "Old body", id: _draftId);
5959
_draftRepo.Setup(r => r.GetByIdAsync(_draftId, default)).ReturnsAsync(draft);
6060

6161
var handler = new UpdateDraftCommandHandler(_draftRepo.Object);
@@ -90,7 +90,7 @@ await Assert.ThrowsAsync<NotFoundException>(() =>
9090
[Fact]
9191
public async Task UpdateDraft_WrongOwner_ThrowsForbidden()
9292
{
93-
var draft = new Draft { Id = _draftId, UserId = Guid.NewGuid() };
93+
var draft = Draft.Create(Guid.NewGuid(), "X", "Y", id: _draftId);
9494
_draftRepo.Setup(r => r.GetByIdAsync(_draftId, default)).ReturnsAsync(draft);
9595

9696
await Assert.ThrowsAsync<ForbiddenException>(() =>
@@ -101,7 +101,7 @@ await Assert.ThrowsAsync<ForbiddenException>(() =>
101101
[Fact]
102102
public async Task DeleteDraft_ExistingOwnedDraft_DeletesAndReturnsTrue()
103103
{
104-
var draft = new Draft { Id = _draftId, UserId = _userId };
104+
var draft = Draft.Create(_userId, "Delete", "Body", id: _draftId);
105105
_draftRepo.Setup(r => r.GetByIdAsync(_draftId, default)).ReturnsAsync(draft);
106106

107107
var result = await new DeleteDraftCommandHandler(_draftRepo.Object)
@@ -126,7 +126,7 @@ await Assert.ThrowsAsync<NotFoundException>(() =>
126126
[Fact]
127127
public async Task DeleteDraft_WrongOwner_ThrowsForbidden()
128128
{
129-
var draft = new Draft { Id = _draftId, UserId = Guid.NewGuid() };
129+
var draft = Draft.Create(Guid.NewGuid(), "S", "B", id: _draftId);
130130
_draftRepo.Setup(r => r.GetByIdAsync(_draftId, default)).ReturnsAsync(draft);
131131

132132
await Assert.ThrowsAsync<ForbiddenException>(() =>

MailCore.Application.Tests/Commands/ForwardEmailCommandHandlerTests.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,9 @@ public class ForwardEmailCommandHandlerTests
2929

3030
public ForwardEmailCommandHandlerTests()
3131
{
32-
_sender = new User { Id = _userId, Email = "sender@example.com", Name = "Sender" };
33-
_originalEmail = new Email
34-
{
35-
Id = _originalEmailId,
36-
Subject = "Original Subject",
37-
Body = "Original Body",
38-
ThreadId = Guid.NewGuid()
39-
};
32+
_sender = User.Create("Sender", "sender@example.com", "hash");
33+
_sender.Id = _userId;
34+
_originalEmail = Email.Create(Guid.NewGuid(), "Original Subject", "Original Body", id: _originalEmailId);
4035

4136
_composer = new EmailComposer(
4237
_userRepo.Object,
@@ -62,7 +57,7 @@ public ForwardEmailCommandHandlerTests()
6257

6358
private void SetupRecipient(string email)
6459
{
65-
var user = new User { Id = Guid.NewGuid(), Email = email };
60+
var user = User.Create("", email, "");
6661
_userRepo.Setup(r => r.GetByEmailAsync(email, default)).ReturnsAsync(user);
6762
}
6863

@@ -86,7 +81,7 @@ public async Task Handle_ValidCommand_CreatesNewThreadAndEmail()
8681
[Fact]
8782
public async Task Handle_SubjectAlreadyHasFwd_DoesNotDuplicatePrefix()
8883
{
89-
var originalWithFwd = new Email { Id = _originalEmailId, Subject = "Fwd: Original Subject", ThreadId = Guid.NewGuid() };
84+
var originalWithFwd = Email.Create(Guid.NewGuid(), "Fwd: Original Subject", "", id: _originalEmailId);
9085
_emailRepo.Setup(r => r.GetByIdAsync(_originalEmailId, default)).ReturnsAsync(originalWithFwd);
9186
_userRepo.Setup(r => r.GetByIdAsync(_userId, default)).ReturnsAsync(_sender);
9287
SetupRecipient("recipient@example.com");

MailCore.Application.Tests/Commands/LabelCommandHandlerTests.cs

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using MailCore.Application.DTOs.Labels;
77
using MailCore.Application.Exceptions;
88
using MailCore.Domain.Entities;
9+
using MailCore.Domain.Enums;
910
using MailCore.Domain.Interfaces;
1011
using Moq;
1112

@@ -47,7 +48,7 @@ public async Task CreateLabel_NullColor_DefaultsToEmptyString()
4748
[Fact]
4849
public async Task UpdateLabel_OwnedLabel_UpdatesAndReturnsTrue()
4950
{
50-
var label = new Label { Id = _labelId, UserId = _userId, Name = "Old", Color = "blue" };
51+
var label = Label.Create(_userId, "Old", "blue", id: _labelId);
5152
_labelRepo.Setup(r => r.GetByIdAsync(_labelId, default)).ReturnsAsync(label);
5253

5354
var result = await new UpdateLabelCommandHandler(_labelRepo.Object)
@@ -71,7 +72,7 @@ public async Task UpdateLabel_NotFound_ThrowsNotFound()
7172
[Fact]
7273
public async Task UpdateLabel_WrongOwner_ThrowsForbidden()
7374
{
74-
var label = new Label { Id = _labelId, UserId = Guid.NewGuid() };
75+
var label = Label.Create(Guid.NewGuid(), "X", "red", id: _labelId);
7576
_labelRepo.Setup(r => r.GetByIdAsync(_labelId, default)).ReturnsAsync(label);
7677

7778
await Assert.ThrowsAsync<ForbiddenException>(() => new UpdateLabelCommandHandler(_labelRepo.Object)
@@ -81,7 +82,7 @@ public async Task UpdateLabel_WrongOwner_ThrowsForbidden()
8182
[Fact]
8283
public async Task DeleteLabel_OwnedLabel_DeletesAndReturnsTrue()
8384
{
84-
var label = new Label { Id = _labelId, UserId = _userId };
85+
var label = Label.Create(_userId, "X", "red", id: _labelId);
8586
_labelRepo.Setup(r => r.GetByIdAsync(_labelId, default)).ReturnsAsync(label);
8687

8788
var result = await new DeleteLabelCommandHandler(_labelRepo.Object)
@@ -105,7 +106,7 @@ public async Task DeleteLabel_NotFound_ThrowsNotFound()
105106
[Fact]
106107
public async Task DeleteLabel_WrongOwner_ThrowsForbidden()
107108
{
108-
var label = new Label { Id = _labelId, UserId = Guid.NewGuid() };
109+
var label = Label.Create(Guid.NewGuid(), "X", "red", id: _labelId);
109110
_labelRepo.Setup(r => r.GetByIdAsync(_labelId, default)).ReturnsAsync(label);
110111

111112
await Assert.ThrowsAsync<ForbiddenException>(() => new DeleteLabelCommandHandler(_labelRepo.Object)
@@ -117,8 +118,8 @@ public async Task DeleteLabel_WrongOwner_ThrowsForbidden()
117118
[Fact]
118119
public async Task AssignLabel_ValidOwnership_AddsLabelAndReturnsTrue()
119120
{
120-
var label = new Label { Id = _labelId, UserId = _userId };
121-
var mr = new MailRecipient { Id = _mailId, UserId = _userId, Labels = [] };
121+
var label = Label.Create(_userId, "Work", "#FF5733", id: _labelId);
122+
var mr = MailRecipient.Create(_userId, Guid.NewGuid(), RecipientType.To, DateTime.UtcNow, id: _mailId);
122123

123124
_labelRepo.Setup(r => r.GetByIdAsync(_labelId, default)).ReturnsAsync(label);
124125
_mailRecipientRepo.Setup(r => r.GetByIdAsync(_mailId, default)).ReturnsAsync(mr);
@@ -133,13 +134,9 @@ public async Task AssignLabel_ValidOwnership_AddsLabelAndReturnsTrue()
133134
[Fact]
134135
public async Task AssignLabel_AlreadyAssigned_ReturnsTrueWithoutDuplicate()
135136
{
136-
var label = new Label { Id = _labelId, UserId = _userId };
137-
var mr = new MailRecipient
138-
{
139-
Id = _mailId,
140-
UserId = _userId,
141-
Labels = [new MailRecipientLabel { LabelId = _labelId }]
142-
};
137+
var label = Label.Create(_userId, "Work", "#FF5733", id: _labelId);
138+
var mr = MailRecipient.Create(_userId, Guid.NewGuid(), RecipientType.To, DateTime.UtcNow, id: _mailId);
139+
mr.Labels.Add(MailRecipientLabel.Create(_mailId, _labelId));
143140

144141
_labelRepo.Setup(r => r.GetByIdAsync(_labelId, default)).ReturnsAsync(label);
145142
_mailRecipientRepo.Setup(r => r.GetByIdAsync(_mailId, default)).ReturnsAsync(mr);
@@ -153,7 +150,7 @@ public async Task AssignLabel_AlreadyAssigned_ReturnsTrueWithoutDuplicate()
153150
public async Task AssignLabel_LabelNotOwned_ReturnsFalse()
154151
{
155152
_labelRepo.Setup(r => r.GetByIdAsync(_labelId, default))
156-
.ReturnsAsync(new Label { Id = _labelId, UserId = Guid.NewGuid() });
153+
.ReturnsAsync(Label.Create(Guid.NewGuid(), "X", "red", id: _labelId));
157154

158155
Assert.False(await new AssignLabelCommandHandler(_labelRepo.Object, _mailRecipientRepo.Object)
159156
.Handle(new AssignLabelCommand(_userId, _mailId, _labelId), default));
@@ -163,9 +160,9 @@ public async Task AssignLabel_LabelNotOwned_ReturnsFalse()
163160
public async Task AssignLabel_MailNotOwned_ReturnsFalse()
164161
{
165162
_labelRepo.Setup(r => r.GetByIdAsync(_labelId, default))
166-
.ReturnsAsync(new Label { Id = _labelId, UserId = _userId });
163+
.ReturnsAsync(Label.Create(_userId, "X", "red", id: _labelId));
167164
_mailRecipientRepo.Setup(r => r.GetByIdAsync(_mailId, default))
168-
.ReturnsAsync(new MailRecipient { Id = _mailId, UserId = Guid.NewGuid() });
165+
.ReturnsAsync(MailRecipient.Create(Guid.NewGuid(), Guid.NewGuid(), RecipientType.To, DateTime.UtcNow, id: _mailId));
169166

170167
Assert.False(await new AssignLabelCommandHandler(_labelRepo.Object, _mailRecipientRepo.Object)
171168
.Handle(new AssignLabelCommand(_userId, _mailId, _labelId), default));
@@ -174,12 +171,8 @@ public async Task AssignLabel_MailNotOwned_ReturnsFalse()
174171
[Fact]
175172
public async Task UnassignLabel_AssignedLabel_RemovesAndReturnsTrue()
176173
{
177-
var mr = new MailRecipient
178-
{
179-
Id = _mailId,
180-
UserId = _userId,
181-
Labels = [new MailRecipientLabel { LabelId = _labelId, MailRecipientId = _mailId }]
182-
};
174+
var mr = MailRecipient.Create(_userId, Guid.NewGuid(), RecipientType.To, DateTime.UtcNow, id: _mailId);
175+
mr.Labels.Add(MailRecipientLabel.Create(_mailId, _labelId));
183176
_mailRecipientRepo.Setup(r => r.GetByIdAsync(_mailId, default)).ReturnsAsync(mr);
184177

185178
var result = await new UnassignLabelCommandHandler(_mailRecipientRepo.Object)
@@ -192,7 +185,7 @@ public async Task UnassignLabel_AssignedLabel_RemovesAndReturnsTrue()
192185
[Fact]
193186
public async Task UnassignLabel_LabelNotAssigned_ReturnsTrueWithoutChange()
194187
{
195-
var mr = new MailRecipient { Id = _mailId, UserId = _userId, Labels = [] };
188+
var mr = MailRecipient.Create(_userId, Guid.NewGuid(), RecipientType.To, DateTime.UtcNow, id: _mailId);
196189
_mailRecipientRepo.Setup(r => r.GetByIdAsync(_mailId, default)).ReturnsAsync(mr);
197190

198191
Assert.True(await new UnassignLabelCommandHandler(_mailRecipientRepo.Object)
@@ -203,7 +196,7 @@ public async Task UnassignLabel_LabelNotAssigned_ReturnsTrueWithoutChange()
203196
public async Task UnassignLabel_MailNotOwned_ReturnsFalse()
204197
{
205198
_mailRecipientRepo.Setup(r => r.GetByIdAsync(_mailId, default))
206-
.ReturnsAsync(new MailRecipient { Id = _mailId, UserId = Guid.NewGuid() });
199+
.ReturnsAsync(MailRecipient.Create(Guid.NewGuid(), Guid.NewGuid(), RecipientType.To, DateTime.UtcNow, id: _mailId));
207200

208201
Assert.False(await new UnassignLabelCommandHandler(_mailRecipientRepo.Object)
209202
.Handle(new UnassignLabelCommand(_userId, _mailId, _labelId), default));

MailCore.Application.Tests/Commands/MailboxCommandHandlerTests.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using MailCore.Application.Commands.Mailbox.Unstar;
99
using MailCore.Application.Exceptions;
1010
using MailCore.Domain.Entities;
11+
using MailCore.Domain.Enums;
1112
using MailCore.Domain.Interfaces;
1213
using Moq;
1314

@@ -20,7 +21,7 @@ public class MailboxCommandHandlerTests
2021
private readonly Guid _mailId = Guid.NewGuid();
2122

2223
private MailRecipient OwnedMail() =>
23-
new() { Id = _mailId, UserId = _userId, IsRead = false, IsStarred = false, IsSpam = false };
24+
MailRecipient.Create(_userId, Guid.NewGuid(), RecipientType.To, DateTime.UtcNow, id: _mailId);
2425

2526
private void SetupRepo(MailRecipient? mail) =>
2627
_repo.Setup(r => r.GetByIdAsync(_mailId, default)).ReturnsAsync(mail);
@@ -51,7 +52,7 @@ await Assert.ThrowsAsync<NotFoundException>(() =>
5152
[Fact]
5253
public async Task MarkRead_WrongOwner_ThrowsForbidden()
5354
{
54-
SetupRepo(new MailRecipient { Id = _mailId, UserId = Guid.NewGuid() });
55+
SetupRepo(MailRecipient.Create(Guid.NewGuid(), Guid.NewGuid(), RecipientType.To, DateTime.UtcNow, id: _mailId));
5556

5657
await Assert.ThrowsAsync<ForbiddenException>(() =>
5758
new MarkMailReadCommandHandler(_repo.Object)
@@ -62,7 +63,7 @@ await Assert.ThrowsAsync<ForbiddenException>(() =>
6263
public async Task MarkUnread_OwnedMail_SetsIsReadFalse()
6364
{
6465
var mail = OwnedMail();
65-
mail.IsRead = true;
66+
mail.MarkAsRead();
6667
SetupRepo(mail);
6768

6869
var result = await new MarkMailUnreadCommandHandler(_repo.Object)
@@ -89,7 +90,7 @@ public async Task MarkStarred_OwnedMail_SetsIsStarredTrue()
8990
public async Task Unstar_OwnedMail_SetsIsStarredFalse()
9091
{
9192
var mail = OwnedMail();
92-
mail.IsStarred = true;
93+
mail.MarkAsStarred();
9394
SetupRepo(mail);
9495

9596
Assert.True(await new UnstarMailCommandHandler(_repo.Object)
@@ -114,7 +115,7 @@ public async Task MarkSpam_OwnedMail_SetsIsSpamTrue()
114115
public async Task Unspam_OwnedMail_SetsIsSpamFalse()
115116
{
116117
var mail = OwnedMail();
117-
mail.IsSpam = true;
118+
mail.MarkAsSpam();
118119
SetupRepo(mail);
119120

120121
Assert.True(await new UnspamMailCommandHandler(_repo.Object)
@@ -150,7 +151,7 @@ await Assert.ThrowsAsync<NotFoundException>(() =>
150151
public async Task Restore_OwnedDeletedMail_ClearsDeletedAt()
151152
{
152153
var mail = OwnedMail();
153-
mail.DeletedAt = DateTime.UtcNow;
154+
mail.SoftDelete();
154155
SetupRepo(mail);
155156

156157
Assert.True(await new RestoreMailCommandHandler(_repo.Object)

0 commit comments

Comments
 (0)