From 1d4d0a992c13f07189f7ea863b5c1e3336902bc2 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 31 May 2023 07:18:34 +0700 Subject: [PATCH 1/6] add: integration test and implementation --- src/Api/Controllers/RoomsController.cs | 1 + .../Common/Extensions/StringExtensions.cs | 13 +++ .../Rooms/Queries/GetAllRoomsPaginated.cs | 39 +++++++++ .../CustomApiFactory.cs | 2 +- .../Queries/GetAllRoomsPaginatedTests.cs | 84 +++++++++++++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/Application/Common/Extensions/StringExtensions.cs create mode 100644 tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index d46390cc..61821074 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -36,6 +36,7 @@ public async Task>> GetById([FromRoute] Guid roomId /// /// Get all rooms paginated details /// A paginated list of rooms + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] 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/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index 8094f631..4c52fa2f 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -1,5 +1,10 @@ +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 MediatR; namespace Application.Rooms.Queries; @@ -13,4 +18,38 @@ 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 rooms = _context.Rooms.AsQueryable(); + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(RoomDto.Id); + } + + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page ?? 1; + var sizeNumber = request.Size ?? 5; + + var result = await rooms + .ProjectTo(_mapper.ConfigurationProvider) + .OrderByCustom(sortBy, sortOrder) + .PaginatedListAsync(pageNumber, sizeNumber); + + 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/Rooms/Queries/GetAllRoomsPaginatedTests.cs b/tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs new file mode 100644 index 00000000..83c6c955 --- /dev/null +++ b/tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs @@ -0,0 +1,84 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Rooms.Queries; +using AutoMapper; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Rooms.Queries; + +public class GetAllRoomsPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllRoomsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(config => config.AddProfile()); + + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnAllRooms() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var room1 = CreateRoom(department1); + var room2 = CreateRoom(department2); + await AddAsync(room1); + await AddAsync(room2); + + var query = new GetAllRoomsPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .ContainEquivalentOf(_mapper.Map(room1), + config => config.IgnoringCyclicReferences()); + result.Items.Should() + .ContainEquivalentOf(_mapper.Map(room2), + config => config.IgnoringCyclicReferences()); + + // Cleanup + Remove(room1); + Remove(room2); + Remove(department1); + Remove(department2); + } + + [Fact] + public async Task ShouldReturnOrderById_WhenWrongSortByIsProvided() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var room1 = CreateRoom(department1); + var room2 = CreateRoom(department2); + await AddAsync(room1); + await AddAsync(room2); + + var query = new GetAllRoomsPaginated.Query() + { + SortBy = "e", + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .BeEquivalentTo(_mapper.Map(new[] { room1, room2 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(room1); + Remove(room2); + Remove(department1); + Remove(department2); + } +} \ No newline at end of file From 87d1dbd2fa23f8a7acecf61ae3d7d78889e13d13 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 31 May 2023 07:24:08 +0700 Subject: [PATCH 2/6] fix: my life --- 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 af9f1bb257e64a7a943e5857c7c61091fb351a54 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 31 May 2023 08:34:49 +0700 Subject: [PATCH 3/6] fix: my life again --- src/Application/Rooms/Queries/GetAllRoomsPaginated.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index 4c52fa2f..513b2122 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -41,13 +41,13 @@ public async Task> Handle(Query request, CancellationToke } var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page ?? 1; - var sizeNumber = request.Size ?? 5; + 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 rooms .ProjectTo(_mapper.ConfigurationProvider) .OrderByCustom(sortBy, sortOrder) - .PaginatedListAsync(pageNumber, sizeNumber); + .PaginatedListAsync(pageNumber.Value, sizeNumber.Value); return result; } From a78e408572de610a54c63d098ccecded08b017b5 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 31 May 2023 13:13:18 +0700 Subject: [PATCH 4/6] add: search --- .../Lockers/GetAllLockersPaginatedQueryParameters.cs | 4 ++++ .../Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs | 4 ++++ src/Api/Controllers/RoomsController.cs | 3 ++- src/Application/Rooms/Queries/GetAllRoomsPaginated.cs | 7 +++++++ tests/Application.Tests.Integration/CustomApiFactory.cs | 2 +- 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs index 656b96f6..9f701ed4 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs @@ -12,6 +12,10 @@ public class GetAllLockersPaginatedQueryParameters /// public Guid? RoomId { get; set; } /// + /// Search term + /// + public string? SearchTerm { get; set; } + /// /// Page number /// public int? Page { get; set; } diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs index 34721c39..89a7bbb6 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -5,6 +5,10 @@ namespace Api.Controllers.Payload.Requests.Rooms; /// public class GetAllRoomsPaginatedQueryParameters { + /// + /// Search term + /// + public string? SearchTerm { get; set; } /// /// Page number /// diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 61821074..5ecb848d 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -41,10 +41,11 @@ public async Task>> GetById([FromRoute] Guid roomId [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task>>> GetAllPaginated( - [FromQuery] GetAllLockersPaginatedQueryParameters queryParameters) + [FromQuery] GetAllRoomsPaginatedQueryParameters queryParameters) { var query = new GetAllRoomsPaginated.Query() { + SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index 513b2122..663f4b6a 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -13,6 +13,7 @@ public class GetAllRoomsPaginated { public record Query : IRequest> { + public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } @@ -34,6 +35,12 @@ public async Task> Handle(Query request, CancellationToke { var rooms = _context.Rooms.AsQueryable(); + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + rooms = rooms.Where(x => + x.Name.Contains(request.SearchTerm, StringComparison.InvariantCultureIgnoreCase)); + } + var sortBy = request.SortBy; if (sortBy is null || !sortBy.MatchesPropertyName()) { 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()); }); }); } From b59ffa1523e7fb018847e7104417d00589c8d7cc Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 31 May 2023 13:17:39 +0700 Subject: [PATCH 5/6] forgive: me --- 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 d1c1bd9f5407d0472f58480898600148d0be9acd Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 1 Jun 2023 11:52:27 +0700 Subject: [PATCH 6/6] fix: search term untranslatable --- src/Application/Rooms/Queries/GetAllRoomsPaginated.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index 663f4b6a..aef45ae6 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -38,7 +38,7 @@ public async Task> Handle(Query request, CancellationToke if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { rooms = rooms.Where(x => - x.Name.Contains(request.SearchTerm, StringComparison.InvariantCultureIgnoreCase)); + x.Name.ToLower().Contains(request.SearchTerm.ToLower())); } var sortBy = request.SortBy; @@ -46,7 +46,6 @@ public async Task> Handle(Query request, CancellationToke { sortBy = nameof(RoomDto.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;