From e69d1abd9ca530392a27c879db0e00541e910ccc Mon Sep 17 00:00:00 2001 From: Roman Kholod Date: Tue, 3 Mar 2026 23:02:00 +0200 Subject: [PATCH] Innitial implementation of nested comments/replies --- .../DTO/Streetcode/Comments/CommentDTO.cs | 4 +- .../Streetcode/Comments/CreateCommentDTO.cs | 4 +- .../Streetcode/Comments/CommentProfile.cs | 13 ++- .../Create/CreateCommentDTOValidator.cs | 7 +- .../Comments/Create/CreateCommentHandler.cs | 15 ++- .../Comments/Delete/DeleteCommentHandler.cs | 7 +- .../GetCommentsByStreetcodeIdHandler.cs | 13 ++- .../Comments/Update/UpdateCommentHandler.cs | 4 +- .../Entities/Streetcode/Comments/Comment.cs | 8 +- .../Persistence/StreetcodeDbContext.cs | 16 +++ .../Streetcode/Comments/CommentController.cs | 2 +- .../Create/CreateCommentHandlerTests.cs | 106 +++++++++--------- .../Delete/DeleteCommentHandlerTests.cs | 20 ++-- .../GetCommentsByStreetcodeIdHandlerTests.cs | 66 +++++++---- 14 files changed, 182 insertions(+), 103 deletions(-) diff --git a/Streetcode/Streetcode.BLL/DTO/Streetcode/Comments/CommentDTO.cs b/Streetcode/Streetcode.BLL/DTO/Streetcode/Comments/CommentDTO.cs index fac9c2a..40b12cf 100644 --- a/Streetcode/Streetcode.BLL/DTO/Streetcode/Comments/CommentDTO.cs +++ b/Streetcode/Streetcode.BLL/DTO/Streetcode/Comments/CommentDTO.cs @@ -9,5 +9,7 @@ public class CommentDTO public int StreetcodeId { get; set; } public string UserId { get; set; } public string UserFullName { get; set; } + public int? ParentCommentId { get; set; } + public IEnumerable Replies { get; set; } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/DTO/Streetcode/Comments/CreateCommentDTO.cs b/Streetcode/Streetcode.BLL/DTO/Streetcode/Comments/CreateCommentDTO.cs index e6c76da..51177e5 100644 --- a/Streetcode/Streetcode.BLL/DTO/Streetcode/Comments/CreateCommentDTO.cs +++ b/Streetcode/Streetcode.BLL/DTO/Streetcode/Comments/CreateCommentDTO.cs @@ -5,5 +5,7 @@ public class CreateCommentDTO public string TextContent { get; set; } public int StreetcodeId { get; set; } + + public int? ParentCommentId { get; set; } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Mapping/Streetcode/Comments/CommentProfile.cs b/Streetcode/Streetcode.BLL/Mapping/Streetcode/Comments/CommentProfile.cs index 2d69a9a..d404ddd 100644 --- a/Streetcode/Streetcode.BLL/Mapping/Streetcode/Comments/CommentProfile.cs +++ b/Streetcode/Streetcode.BLL/Mapping/Streetcode/Comments/CommentProfile.cs @@ -10,13 +10,18 @@ public CommentProfile() { CreateMap() .ForMember(dest => dest.UserFullName, opt => opt.MapFrom(src => - src.User != null ? $"{src.User.Name} {src.User.Surname}" : "Unknown User")); + src.User != null ? $"{src.User.Name} {src.User.Surname}" : "Unknown User")) + .ForMember(dest => dest.ParentCommentId, opt => opt.MapFrom(src => src.ParentCommentId)) + .ForMember(dest => dest.Replies, opt => opt.MapFrom(src => src.Replies)); + + CreateMap() + .ForMember(dest => dest.ParentCommentId, opt => opt.MapFrom(src => src.ParentCommentId)); - CreateMap(); CreateMap() .ForMember(dest => dest.UserId, opt => opt.Ignore()) .ForMember(dest => dest.StreetcodeId, opt => opt.Ignore()) - .ForMember(dest => dest.CreatedAt, opt => opt.Ignore()); + .ForMember(dest => dest.CreatedAt, opt => opt.Ignore()) + .ForMember(dest => dest.ParentCommentId, opt => opt.Ignore()); } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Create/CreateCommentDTOValidator.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Create/CreateCommentDTOValidator.cs index 1275b46..5bae851 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Create/CreateCommentDTOValidator.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Create/CreateCommentDTOValidator.cs @@ -17,6 +17,11 @@ public CreateCommentDTOValidator() RuleFor(x => x.StreetcodeId) .GreaterThan(0).WithMessage(Messages.Error_PropertyMustBeGreaterThanZero.Format(nameof(CreateCommentDTO.StreetcodeId))); + + RuleFor(x => x.ParentCommentId) + .GreaterThan(0) + .When(x => x.ParentCommentId.HasValue) + .WithMessage(Messages.Error_PropertyMustBeGreaterThanZero.Format(nameof(CreateCommentDTO.ParentCommentId))); } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Create/CreateCommentHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Create/CreateCommentHandler.cs index 060ac3a..70acfad 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Create/CreateCommentHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Create/CreateCommentHandler.cs @@ -25,6 +25,19 @@ public CreateCommentHandler(IRepositoryWrapper repositoryWrapper, IMapper mapper public async Task> Handle(CreateCommentCommand command, CancellationToken cancellationToken) { + if (command.Comment.ParentCommentId.HasValue) + { + var parentComment = await _repositoryWrapper.CommentRepository + .GetFirstOrDefaultAsync(c => c.Id == command.Comment.ParentCommentId.Value); + + if (parentComment == null) + { + string errorParentMsg = "Parent comment not found."; + _logger.LogError(command, errorParentMsg); + return Result.Fail(new Error(errorParentMsg)); + } + } + var comment = _mapper.Map(command.Comment); comment.UserId = command.UserId; @@ -48,4 +61,4 @@ public async Task> Handle(CreateCommentCommand command, Cance return Result.Fail(new Error(errorMsg)); } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Delete/DeleteCommentHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Delete/DeleteCommentHandler.cs index ad8edae..561b32b 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Delete/DeleteCommentHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Delete/DeleteCommentHandler.cs @@ -1,5 +1,6 @@ using FluentResults; using MediatR; +using Microsoft.EntityFrameworkCore; using Streetcode.BLL.Interfaces.Logging; using Streetcode.DAL.Entities.Streetcode.Comments; using Streetcode.DAL.Repositories.Interfaces.Base; @@ -22,7 +23,9 @@ public DeleteCommentHandler(IRepositoryWrapper repositoryWrapper, ILoggerService public async Task> Handle(DeleteCommentCommand command, CancellationToken cancellationToken) { var comment = await _repositoryWrapper.CommentRepository - .GetFirstOrDefaultAsync(t => t.Id == command.Id); + .GetFirstOrDefaultAsync( + predicate: t => t.Id == command.Id, + include: x => x.Include(c => c.Replies)); if (comment is null) { @@ -54,4 +57,4 @@ public async Task> Handle(DeleteCommentCommand command, Cancellatio return Result.Fail(new Error(errorMsg)); } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/GetByStreetcodeId/GetCommentsByStreetcodeIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/GetByStreetcodeId/GetCommentsByStreetcodeIdHandler.cs index 4c48ab2..ef68dd5 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/GetByStreetcodeId/GetCommentsByStreetcodeIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/GetByStreetcodeId/GetCommentsByStreetcodeIdHandler.cs @@ -25,11 +25,16 @@ public async Task>> Handle(GetCommentsByStreetcod { var comments = await _repositoryWrapper.CommentRepository.GetAllAsync( predicate: c => c.StreetcodeId == request.StreetcodeId, - include: x => x.Include(c => c.User)); + include: x => x.Include(c => c.User).Include(c => c.Replies)); - var sortedComments = comments.OrderByDescending(c => c.CreatedAt); + var allMappedComments = _mapper.Map>(comments); - return Result.Ok(_mapper.Map>(sortedComments)); + var rootComments = allMappedComments + .Where(c => c.ParentCommentId == null) + .OrderByDescending(c => c.CreatedAt) + .AsEnumerable(); + + return Result.Ok(rootComments); } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Update/UpdateCommentHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Update/UpdateCommentHandler.cs index 294d0f7..c3c6bc7 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Update/UpdateCommentHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Comments/Update/UpdateCommentHandler.cs @@ -47,7 +47,7 @@ public async Task> Handle(UpdateCommentCommand command, Cance return Result.Fail(new Error(errorAuthMsg)); } - comment = _mapper.Map(command.Comment, comment); + _mapper.Map(command.Comment, comment); comment.UpdatedAt = DateTime.UtcNow; _repositoryWrapper.CommentRepository.Update(comment); @@ -63,4 +63,4 @@ public async Task> Handle(UpdateCommentCommand command, Cance return Result.Fail(new Error(errorMsg)); } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.DAL/Entities/Streetcode/Comments/Comment.cs b/Streetcode/Streetcode.DAL/Entities/Streetcode/Comments/Comment.cs index 05311c4..1aa8c1e 100644 --- a/Streetcode/Streetcode.DAL/Entities/Streetcode/Comments/Comment.cs +++ b/Streetcode/Streetcode.DAL/Entities/Streetcode/Comments/Comment.cs @@ -29,5 +29,11 @@ public class Comment public string UserId { get; set; } public User? User { get; set; } + + public int? ParentCommentId { get; set; } + + public Comment? ParentComment { get; set; } + + public ICollection Replies { get; set; } = new List(); } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.DAL/Persistence/StreetcodeDbContext.cs b/Streetcode/Streetcode.DAL/Persistence/StreetcodeDbContext.cs index 6e4dc5f..254d778 100644 --- a/Streetcode/Streetcode.DAL/Persistence/StreetcodeDbContext.cs +++ b/Streetcode/Streetcode.DAL/Persistence/StreetcodeDbContext.cs @@ -320,5 +320,21 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithOne(t => t.User) .HasForeignKey(t => t.UserId) .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity(entity => + { + entity.HasOne(c => c.ParentComment) + .WithMany(c => c.Replies) + .HasForeignKey(c => c.ParentCommentId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(c => c.Streetcode) + .WithMany(s => s.Comments) + .HasForeignKey(c => c.StreetcodeId); + + entity.HasOne(c => c.User) + .WithMany(u => u.Comments) + .HasForeignKey(c => c.UserId); + }); } } diff --git a/Streetcode/Streetcode.WebApi/Controllers/Streetcode/Comments/CommentController.cs b/Streetcode/Streetcode.WebApi/Controllers/Streetcode/Comments/CommentController.cs index 8803353..30f5fba 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Streetcode/Comments/CommentController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Streetcode/Comments/CommentController.cs @@ -65,4 +65,4 @@ public async Task Delete([FromRoute] int id) { return User.FindFirst(ClaimTypes.NameIdentifier)?.Value; } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Comments/Create/CreateCommentHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Comments/Create/CreateCommentHandlerTests.cs index 90954ea..21bcf10 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Comments/Create/CreateCommentHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Comments/Create/CreateCommentHandlerTests.cs @@ -59,72 +59,62 @@ public async Task Handle_ReturnsSuccess_WhenRequestIsValid() var userId = "user-123"; var command = new CreateCommentCommand(createDto, userId); - var commentEntity = new Comment - { - Id = 1, - TextContent = createDto.TextContent, - StreetcodeId = createDto.StreetcodeId, - UserId = userId, - }; - - this.commentRepositoryMock.Setup(x => x.CreateAsync(It.IsAny())) - .ReturnsAsync(commentEntity); - - this.repositoryWrapperMock.Setup(x => x.SaveChangesAsync()) - .ReturnsAsync(1); - - this.userRepositoryMock - .Setup(x => x.GetFirstOrDefaultAsync( - It.IsAny>>(), - null, - It.IsAny())) - .ReturnsAsync(new User()); + this.SetupStandardSuccessMocks(userId, createDto.TextContent); // Act var result = await this.handler.Handle(command, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); - result.Value.Should().NotBeNull(); result.Value.TextContent.Should().Be(createDto.TextContent); - - this.commentRepositoryMock.Verify(x => x.CreateAsync(It.IsAny()), Times.Once); - this.repositoryWrapperMock.Verify(x => x.SaveChangesAsync(), Times.Once); } [Fact] - public async Task Handle_SetsCorrectUser_WhenRequestIsValid() + public async Task Handle_ReturnsSuccess_WhenReplyIsValid() { // Arrange + int parentId = 1; var createDto = GetCreateCommentDTO(); - var userId = "test-user-id"; + createDto.ParentCommentId = parentId; + var userId = "user-123"; var command = new CreateCommentCommand(createDto, userId); - var user = new User - { - Id = userId, - Name = "TestName", - Surname = "TestSurname", - }; + this.commentRepositoryMock.Setup(x => x.GetFirstOrDefaultAsync( + It.IsAny>>(), null, It.IsAny())) + .ReturnsAsync(new Comment { Id = parentId }); - this.commentRepositoryMock.Setup(x => x.CreateAsync(It.IsAny())) - .ReturnsAsync((Comment c) => c); + this.SetupStandardSuccessMocks(userId, createDto.TextContent); - this.repositoryWrapperMock.Setup(x => x.SaveChangesAsync()).ReturnsAsync(1); + // Act + var result = await this.handler.Handle(command, CancellationToken.None); - this.userRepositoryMock - .Setup(x => x.GetFirstOrDefaultAsync( - It.IsAny>>(), - null, - It.IsAny())) - .ReturnsAsync(user); + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.ParentCommentId.Should().Be(parentId); + this.commentRepositoryMock.Verify(x => x.GetFirstOrDefaultAsync(It.IsAny>>(), null, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_ReturnsFail_WhenParentCommentNotFound() + { + // Arrange + int parentId = 999; + var createDto = GetCreateCommentDTO(); + createDto.ParentCommentId = parentId; + var userId = "user-123"; + var command = new CreateCommentCommand(createDto, userId); + + this.commentRepositoryMock.Setup(x => x.GetFirstOrDefaultAsync( + It.IsAny>>(), null, It.IsAny())) + .ReturnsAsync((Comment)null); // Act var result = await this.handler.Handle(command, CancellationToken.None); // Assert - result.Value.UserId.Should().Be(userId); - result.Value.UserFullName.Should().Be("TestName TestSurname"); + result.IsFailed.Should().BeTrue(); + result.Errors.First().Message.Should().Be("Parent comment not found."); + this.loggerMock.Verify(x => x.LogError(command, "Parent comment not found."), Times.Once); } [Fact] @@ -135,21 +125,33 @@ public async Task Handle_ReturnsFail_WhenSaveChangesFails() var userId = "user-123"; var command = new CreateCommentCommand(createDto, userId); - this.commentRepositoryMock.Setup(x => x.CreateAsync(It.IsAny())); - - this.repositoryWrapperMock.Setup(x => x.SaveChangesAsync()) - .ReturnsAsync(0); - - var expectedErrorMsg = Messages.Error_FailedToCreateEntity.Format(nameof(Comment)); + this.repositoryWrapperMock.Setup(x => x.SaveChangesAsync()).ReturnsAsync(0); // Act var result = await this.handler.Handle(command, CancellationToken.None); // Assert result.IsFailed.Should().BeTrue(); - result.Errors.First().Message.Should().Be(expectedErrorMsg); + this.loggerMock.Verify(x => x.LogError(command, It.IsAny()), Times.Once); + } + + private void SetupStandardSuccessMocks(string userId, string textContent) + { + var commentEntity = new Comment + { + Id = 1, + TextContent = textContent, + UserId = userId, + }; + + this.commentRepositoryMock.Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(commentEntity); + + this.repositoryWrapperMock.Setup(x => x.SaveChangesAsync()).ReturnsAsync(1); - this.loggerMock.Verify(x => x.LogError(command, expectedErrorMsg), Times.Once); + this.userRepositoryMock.Setup(x => x.GetFirstOrDefaultAsync( + It.IsAny>>(), null, It.IsAny())) + .ReturnsAsync(new User { Id = userId, Name = "Name", Surname = "Surname" }); } private static CreateCommentDTO GetCreateCommentDTO() @@ -161,4 +163,4 @@ private static CreateCommentDTO GetCreateCommentDTO() }; } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Comments/Delete/DeleteCommentHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Comments/Delete/DeleteCommentHandlerTests.cs index aecd877..ba7248d 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Comments/Delete/DeleteCommentHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Comments/Delete/DeleteCommentHandlerTests.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using FluentAssertions; using global::MediatR; + using Microsoft.EntityFrameworkCore.Query; using Moq; using Streetcode.BLL.Interfaces.Logging; using Streetcode.BLL.MediatR.Streetcode.Comments.Delete; @@ -42,12 +43,12 @@ public async Task Handle_ReturnsSuccess_WhenUserIsOwnerAndCommentExists() string userId = "user-123"; var command = new DeleteCommentCommand(commentId, userId); - var comment = new Comment { Id = commentId, UserId = userId }; + var comment = new Comment { Id = commentId, UserId = userId, Replies = new List() }; this.commentRepositoryMock .Setup(x => x.GetFirstOrDefaultAsync( It.IsAny>>(), - null, + It.IsAny, IIncludableQueryable>>(), It.IsAny())) .ReturnsAsync(comment); @@ -72,7 +73,7 @@ public async Task Handle_ReturnsFail_WhenCommentNotFound() this.commentRepositoryMock .Setup(x => x.GetFirstOrDefaultAsync( It.IsAny>>(), - null, + It.IsAny, IIncludableQueryable>>(), It.IsAny())) .ReturnsAsync((Comment?)null); @@ -84,8 +85,6 @@ public async Task Handle_ReturnsFail_WhenCommentNotFound() // Assert result.IsFailed.Should().BeTrue(); result.Errors.First().Message.Should().Be(expectedError); - - this.commentRepositoryMock.Verify(x => x.Delete(It.IsAny()), Times.Never); } [Fact] @@ -102,7 +101,7 @@ public async Task Handle_ReturnsFail_WhenUserIsNotOwner() this.commentRepositoryMock .Setup(x => x.GetFirstOrDefaultAsync( It.IsAny>>(), - null, + It.IsAny, IIncludableQueryable>>(), It.IsAny())) .ReturnsAsync(comment); @@ -114,8 +113,6 @@ public async Task Handle_ReturnsFail_WhenUserIsNotOwner() // Assert result.IsFailed.Should().BeTrue(); result.Errors.First().Message.Should().Be(expectedError); - - this.commentRepositoryMock.Verify(x => x.Delete(It.IsAny()), Times.Never); } [Fact] @@ -128,20 +125,17 @@ public async Task Handle_ReturnsFail_WhenSaveChangesFails() this.commentRepositoryMock .Setup(x => x.GetFirstOrDefaultAsync( It.IsAny>>(), - null, + It.IsAny, IIncludableQueryable>>(), It.IsAny())) .ReturnsAsync(comment); this.repositoryWrapperMock.Setup(x => x.SaveChangesAsync()).ReturnsAsync(0); - var expectedError = Messages.Error_FailedToDeleteEntity.Format(nameof(Comment)); - // Act var result = await this.handler.Handle(command, CancellationToken.None); // Assert result.IsFailed.Should().BeTrue(); - result.Errors.First().Message.Should().Be(expectedError); } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Comments/GetByStreetcodeId/GetCommentsByStreetcodeIdHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Comments/GetByStreetcodeId/GetCommentsByStreetcodeIdHandlerTests.cs index 71ef7ba..b5b15ee 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Comments/GetByStreetcodeId/GetCommentsByStreetcodeIdHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Comments/GetByStreetcodeId/GetCommentsByStreetcodeIdHandlerTests.cs @@ -42,6 +42,48 @@ public GetCommentsByStreetcodeIdHandlerTests() this.loggerMock.Object); } + [Fact] + public async Task Handle_ReturnsOnlyRootComments_WhenHierarchyExists() + { + // Arrange + int streetcodeId = 1; + var query = new GetCommentsByStreetcodeIdQuery(streetcodeId); + + var reply = new Comment { Id = 2, ParentCommentId = 1, TextContent = "Reply", StreetcodeId = streetcodeId }; + var rootWithReply = new Comment + { + Id = 1, + ParentCommentId = null, + TextContent = "Root with reply", + StreetcodeId = streetcodeId, + Replies = new List { reply } + }; + var standaloneRoot = new Comment { Id = 3, ParentCommentId = null, TextContent = "Standalone", StreetcodeId = streetcodeId }; + + var comments = new List { rootWithReply, reply, standaloneRoot }; + + this.commentRepositoryMock + .Setup(x => x.GetAllAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(comments); + + // Act + var result = await this.handler.Handle(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + + result.Value.Should().HaveCount(2); + result.Value.Should().NotContain(c => c.Id == 2); + + // Verify nesting + var rootResult = result.Value.First(c => c.Id == 1); + rootResult.Replies.Should().HaveCount(1); + rootResult.Replies.First().Id.Should().Be(2); + } + [Fact] public async Task Handle_ReturnsSortedComments_WhenCommentsExist() { @@ -51,21 +93,9 @@ public async Task Handle_ReturnsSortedComments_WhenCommentsExist() var comments = new List { - new () - { - Id = 1, CreatedAt = DateTime.UtcNow.AddDays(-2), - StreetcodeId = streetcodeId, TextContent = "Oldest", - }, - new () - { - Id = 2, CreatedAt = DateTime.UtcNow, - StreetcodeId = streetcodeId, TextContent = "Newest", - }, - new () - { - Id = 3, CreatedAt = DateTime.UtcNow.AddDays(-1), - StreetcodeId = streetcodeId, TextContent = "Middle", - }, + new () { Id = 1, CreatedAt = DateTime.UtcNow.AddDays(-2), StreetcodeId = streetcodeId, TextContent = "Oldest" }, + new () { Id = 2, CreatedAt = DateTime.UtcNow, StreetcodeId = streetcodeId, TextContent = "Newest" }, + new () { Id = 3, CreatedAt = DateTime.UtcNow.AddDays(-1), StreetcodeId = streetcodeId, TextContent = "Middle" }, }; this.commentRepositoryMock @@ -80,11 +110,8 @@ public async Task Handle_ReturnsSortedComments_WhenCommentsExist() // Assert result.IsSuccess.Should().BeTrue(); - result.Value.Should().HaveCount(3); - var resultList = result.Value.ToList(); resultList[0].TextContent.Should().Be("Newest"); - resultList[1].TextContent.Should().Be("Middle"); resultList[2].TextContent.Should().Be("Oldest"); } @@ -106,8 +133,7 @@ public async Task Handle_ReturnsEmptyList_WhenNoCommentsFound() // Assert result.IsSuccess.Should().BeTrue(); - result.Value.Should().NotBeNull(); result.Value.Should().BeEmpty(); } } -} +} \ No newline at end of file