From 15625a1af6301df28f4263a9d3cfa58e4a9a878a Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 31 May 2023 08:32:46 +0700 Subject: [PATCH 1/4] add: integration test and implementation --- .../Common/Extensions/StringExtensions.cs | 13 ++ .../Folders/Queries/GetAllFoldersPaginated.cs | 85 ++++++++++++ .../CustomApiFactory.cs | 2 +- .../Queries/GetAllFoldersPaginatedTests.cs | 131 ++++++++++++++++++ 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 src/Application/Common/Extensions/StringExtensions.cs create mode 100644 tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs diff --git a/src/Application/Common/Extensions/StringExtensions.cs b/src/Application/Common/Extensions/StringExtensions.cs new file mode 100644 index 00000000..b2a723de --- /dev/null +++ b/src/Application/Common/Extensions/StringExtensions.cs @@ -0,0 +1,13 @@ +namespace Application.Common.Extensions; + +public static class StringExtensions +{ + public static bool MatchesPropertyName(this string input) + where T : class + { + var type = typeof(T); + var properties = type.GetProperties(); + + return properties.Any(property => string.Equals(property.Name, input)); + } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 280a4c5c..876cd334 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -1,11 +1,30 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Folders.Queries; public class GetAllFoldersPaginated { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.RoomId) + .Must((query, roomId) => roomId is not null || query.LockerId is null); + } + } + public record Query : IRequest> { public Guid? RoomId { get; init; } @@ -15,4 +34,70 @@ public record Query : IRequest> public string? SortBy { get; init; } public string? SortOrder { get; init; } } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var folders = _context.Folders.AsQueryable(); + var roomExists = request.RoomId is not null; + var lockerExists = request.LockerId is not null; + + if (lockerExists) + { + var locker = await _context.Lockers + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.LockerId + && x.IsAvailable, cancellationToken); + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + if (locker.Room.Id != request.RoomId) + { + throw new ConflictException("Room does not match locker."); + } + + folders = folders.Where(x => x.Locker.Id == request.LockerId); + } + else if (roomExists) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId + && x.IsAvailable, cancellationToken); + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + folders = folders.Where(x => x.Locker.Room.Id == request.RoomId); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(LockerDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var result = await folders + .ProjectTo(_mapper.ConfigurationProvider) + .OrderByCustom(sortBy, sortOrder) + .PaginatedListAsync(pageNumber.Value, sizeNumber.Value); + + return result; + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/CustomApiFactory.cs b/tests/Application.Tests.Integration/CustomApiFactory.cs index f76d4ac3..5d14c8ec 100644 --- a/tests/Application.Tests.Integration/CustomApiFactory.cs +++ b/tests/Application.Tests.Integration/CustomApiFactory.cs @@ -28,7 +28,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) var databaseSettings = GetConfiguration().GetSection(nameof(DatabaseSettings)).Get(); services.AddDbContext(options => { - options.UseNpgsql(databaseSettings!.ConnectionString, optionsBuilder => optionsBuilder.UseNodaTime()); + options.UseNpgsql("Server=localhost;Port=5432;Database=mytestdb;User ID=profiletester;Password=supasupasecured;", optionsBuilder => optionsBuilder.UseNodaTime()); }); }); } diff --git a/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs b/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs new file mode 100644 index 00000000..48377685 --- /dev/null +++ b/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs @@ -0,0 +1,131 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Folders.Queries; +using AutoMapper; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Folders.Queries; + +public class GetAllFoldersPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllFoldersPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(config => config.AddProfile()); + + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnFolders() + { + // Arrange + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var folder3 = CreateFolder(); + var locker1 = CreateLocker(folder1); + var locker2 = CreateLocker(folder2, folder3); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetAllFoldersPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(3); + result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder1, folder2, folder3 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(folder1); + Remove(folder2); + Remove(folder3); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(department); + } + + [Fact] + public async Task ShouldReturnFoldersOfASpecificLocker() + { + // Arrange + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var folder3 = CreateFolder(); + var locker1 = CreateLocker(folder1); + var locker2 = CreateLocker(folder2, folder3); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetAllFoldersPaginated.Query() + { + RoomId = room.Id, + LockerId = locker2.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder2, folder3 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(folder1); + Remove(folder2); + Remove(folder3); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(department); + } + + [Fact] + public async Task ShouldReturnNothing_WhenWrongPaginationDetailsAreProvided() + { + // Arrange + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var folder3 = CreateFolder(); + var locker1 = CreateLocker(folder1); + var locker2 = CreateLocker(folder2, folder3); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetAllFoldersPaginated.Query() + { + RoomId = room.Id, + LockerId = locker2.Id, + Page = -1, + Size = -4, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder2, folder3 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(folder1); + Remove(folder2); + Remove(folder3); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(department); + } +} \ No newline at end of file From 617dd62b5cc183f32cd2aebad52304b9fd9192c2 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 31 May 2023 08:33:05 +0700 Subject: [PATCH 2/4] add: i forgot this --- tests/Application.Tests.Integration/CustomApiFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Application.Tests.Integration/CustomApiFactory.cs b/tests/Application.Tests.Integration/CustomApiFactory.cs index 5d14c8ec..f76d4ac3 100644 --- a/tests/Application.Tests.Integration/CustomApiFactory.cs +++ b/tests/Application.Tests.Integration/CustomApiFactory.cs @@ -28,7 +28,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) var databaseSettings = GetConfiguration().GetSection(nameof(DatabaseSettings)).Get(); services.AddDbContext(options => { - options.UseNpgsql("Server=localhost;Port=5432;Database=mytestdb;User ID=profiletester;Password=supasupasecured;", optionsBuilder => optionsBuilder.UseNodaTime()); + options.UseNpgsql(databaseSettings!.ConnectionString, optionsBuilder => optionsBuilder.UseNodaTime()); }); }); } From 4f7ed28922c5ea40546fdde55d8a3f0544950c69 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 31 May 2023 13:28:20 +0700 Subject: [PATCH 3/4] add: search --- src/Api/Controllers/FoldersController.cs | 1 + .../Folders/GetAllFoldersPaginatedQueryParameters.cs | 4 ++++ src/Application/Folders/Queries/GetAllFoldersPaginated.cs | 7 +++++++ 3 files changed, 12 insertions(+) diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 3365ec81..a08b36cc 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -47,6 +47,7 @@ public async Task>>> GetAllPaginate { RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, + SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, diff --git a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs index 1be3d84f..ae6399ab 100644 --- a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs @@ -14,6 +14,10 @@ public class GetAllFoldersPaginatedQueryParameters /// public Guid? LockerId { get; set; } /// + /// Search term + /// + public string? SearchTerm { get; init; } + /// /// Page number /// public int? Page { get; set; } diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 876cd334..c52fa7dd 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -29,6 +29,7 @@ public record Query : IRequest> { public Guid? RoomId { get; init; } public Guid? LockerId { get; init; } + public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } @@ -82,6 +83,12 @@ public async Task> Handle(Query request, CancellationTo folders = folders.Where(x => x.Locker.Room.Id == request.RoomId); } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + folders = folders.Where(x => + x.Name.Contains(request.SearchTerm, StringComparison.InvariantCultureIgnoreCase)); + } var sortBy = request.SortBy; if (sortBy is null || !sortBy.MatchesPropertyName()) From 60f8689dcf7657dcc675e99ce69cca5bedd3cfa8 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 1 Jun 2023 11:54:41 +0700 Subject: [PATCH 4/4] fix: search term untranslatable --- src/Application/Folders/Queries/GetAllFoldersPaginated.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index c52fa7dd..c34617ce 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -87,7 +87,7 @@ public async Task> Handle(Query request, CancellationTo if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { folders = folders.Where(x => - x.Name.Contains(request.SearchTerm, StringComparison.InvariantCultureIgnoreCase)); + x.Name.ToLower().Contains(request.SearchTerm.ToLower())); } var sortBy = request.SortBy;