From d14784f5d035494c9f15e3ac9f444062e0908323 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Fri, 14 Jun 2024 18:31:23 +0200 Subject: [PATCH 01/12] Add search form to Browse/Ads view --- .../Services/Browse/BrowseServiceTestsBase.cs | 13 ---- .../TestsWithInMemoryDbBase.cs | 14 +++++ .../Browse/BrowseControllerAdsTests.cs | 13 ++++ .../Browse/BrowseControllerTestsBase.cs | 6 +- .../Interfaces/Services/ICategoryService.cs | 7 +++ .../Models/DTOs/AdSearchCriteriaDto.cs | 17 +++++ .../DTOs/Requests/GetBrowseAdsPageRequest.cs | 21 ++++++- .../Responses/GetBrowseAdsPageResponse.cs | 1 + .../Services/CategoryService.cs | 25 ++++++++ .../Controllers/BrowseController.cs | 62 +++++++++++++++---- .../Controllers/TutorController.cs | 26 ++++---- .../Models/GoToAdsPageViewModel.cs | 22 +++++++ TutorLizard.Web/Program.cs | 1 + TutorLizard.Web/Strings/PartialNames.cs | 2 + TutorLizard.Web/Views/Browse/Ads.cshtml | 11 +++- .../Views/Shared/_AdSearchCriteria.cshtml | 60 ++++++++++++++++++ .../Views/Shared/_GoToAdsPage.cshtml | 17 +++++ 17 files changed, 275 insertions(+), 43 deletions(-) create mode 100644 TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs create mode 100644 TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs create mode 100644 TutorLizard.BusinessLogic/Services/CategoryService.cs create mode 100644 TutorLizard.Web/Models/GoToAdsPageViewModel.cs create mode 100644 TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml create mode 100644 TutorLizard.Web/Views/Shared/_GoToAdsPage.cshtml diff --git a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceTestsBase.cs b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceTestsBase.cs index de6e4f8a..753c38f9 100644 --- a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceTestsBase.cs +++ b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceTestsBase.cs @@ -118,17 +118,4 @@ protected List CreateTestScheduleItems(int scheduleItemCount) return scheduleItems; } - - protected IQueryable AddEntitiesToInMemoryDb(List entities) - where TEntity : class - { - DbContext - .Set() - .AddRange(entities); - DbContext.SaveChanges(); - - return DbContext - .Set() - .AsQueryable(); - } } \ No newline at end of file diff --git a/Tests/TutorLizard.BusinessLogic.Tests/TestsWithInMemoryDbBase.cs b/Tests/TutorLizard.BusinessLogic.Tests/TestsWithInMemoryDbBase.cs index 9cc5c53d..913a28df 100644 --- a/Tests/TutorLizard.BusinessLogic.Tests/TestsWithInMemoryDbBase.cs +++ b/Tests/TutorLizard.BusinessLogic.Tests/TestsWithInMemoryDbBase.cs @@ -14,6 +14,20 @@ public void Dispose() { DbContext.Dispose(); } + + protected IQueryable AddEntitiesToInMemoryDb(List entities) + where TEntity : class + { + DbContext + .Set() + .AddRange(entities); + DbContext.SaveChanges(); + + return DbContext + .Set() + .AsQueryable(); + } + private JaszczurContext SetupInMemoryDbContext() { DbContextOptionsBuilder dbBuilder = new(); diff --git a/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerAdsTests.cs b/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerAdsTests.cs index ad626e7e..9cd51ad0 100644 --- a/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerAdsTests.cs +++ b/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerAdsTests.cs @@ -1,12 +1,18 @@ using AutoFixture; using Microsoft.AspNetCore.Mvc; using Moq; +using TutorLizard.BusinessLogic.Models.DTOs; using TutorLizard.BusinessLogic.Models.DTOs.Requests; using TutorLizard.BusinessLogic.Models.DTOs.Responses; namespace TutorLizard.Web.Tests.Controllers.Browse; public class BrowseControllerAdsTests : BrowseControllerTestsBase { + public BrowseControllerAdsTests() : base() + { + SetupMockCategoryServiceGetAll([]); + } + [Theory] [InlineData(0, 1)] [InlineData(-1, 1)] @@ -115,4 +121,11 @@ private GetBrowseAdsPageResponse CreateGetBrowseAdsPageResponse(bool success) .With(r => r.Success, success) .Create(); } + + private void SetupMockCategoryServiceGetAll(List categories) + { + MockCategoryService + .Setup(x => x.GetAllCategories()) + .Returns(Task.FromResult(categories)); + } } diff --git a/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerTestsBase.cs b/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerTestsBase.cs index 4b198338..dfeac267 100644 --- a/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerTestsBase.cs +++ b/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerTestsBase.cs @@ -1,6 +1,8 @@ using AutoFixture; using Moq; +using TutorLizard.BusinessLogic.Interfaces.Data.Repositories; using TutorLizard.BusinessLogic.Interfaces.Services; +using TutorLizard.BusinessLogic.Models; using TutorLizard.Web.Controllers; using TutorLizard.Web.Interfaces.Services; @@ -12,13 +14,15 @@ public abstract class BrowseControllerTestsBase protected Mock MockBrowseService = new(); protected Mock MockUserAuthenticationService = new(); protected Mock MockUiMessagesService = new(); + protected Mock MockCategoryService = new(); protected Fixture Fixture = new(); public BrowseControllerTestsBase() { BrowseController = new(MockBrowseService.Object, MockUserAuthenticationService.Object, - MockUiMessagesService.Object); + MockUiMessagesService.Object, + MockCategoryService.Object); } protected void SetupMockGetLoggedInUserId(int? userId) diff --git a/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs b/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs new file mode 100644 index 00000000..a7910b5f --- /dev/null +++ b/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs @@ -0,0 +1,7 @@ +using TutorLizard.BusinessLogic.Models.DTOs; + +namespace TutorLizard.BusinessLogic.Interfaces.Services; +public interface ICategoryService +{ + Task> GetAllCategories(); +} \ No newline at end of file diff --git a/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs b/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs new file mode 100644 index 00000000..31ae3812 --- /dev/null +++ b/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs @@ -0,0 +1,17 @@ +namespace TutorLizard.BusinessLogic.Models.DTOs; +public class AdSearchCriteriaDto +{ + public bool SearchByText { get; set; } + public bool SearchByPriceMin { get; set; } + public bool SearchByPriceMax { get; set; } + public bool SearchByLocation { get; set; } + public bool SearchByIsRemote { get; set; } + public bool SearchByCategoryId { get; set; } + + public string Text { get; set; } = ""; + public decimal PriceMin { get; set; } = 0; + public decimal PriceMax { get; set; } = 0; + public string Location { get; set; } = ""; + public bool IsRemote { get; set; } = false; + public int CategoryId { get; set; } = 1; +} diff --git a/TutorLizard.BusinessLogic/Models/DTOs/Requests/GetBrowseAdsPageRequest.cs b/TutorLizard.BusinessLogic/Models/DTOs/Requests/GetBrowseAdsPageRequest.cs index 5d401349..09288150 100644 --- a/TutorLizard.BusinessLogic/Models/DTOs/Requests/GetBrowseAdsPageRequest.cs +++ b/TutorLizard.BusinessLogic/Models/DTOs/Requests/GetBrowseAdsPageRequest.cs @@ -1,6 +1,21 @@ namespace TutorLizard.BusinessLogic.Models.DTOs.Requests; -public class GetBrowseAdsPageRequest(int pageNumber, int pageSize) +public class GetBrowseAdsPageRequest { - public int PageNumber { get; set; } = pageNumber; - public int PageSize { get; set; } = pageSize; + public int PageNumber { get; set; } + public int PageSize { get; set; } + public AdSearchCriteriaDto SearchCriteria { get; set; } + + + public GetBrowseAdsPageRequest(int pageNumber, int pageSize) + { + PageNumber = pageNumber; + PageSize = pageSize; + SearchCriteria = new(); + } + public GetBrowseAdsPageRequest(int pageNumber, int pageSize, AdSearchCriteriaDto searchCriteria) + { + PageNumber = pageNumber; + PageSize = pageSize; + SearchCriteria = searchCriteria; + } } diff --git a/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs b/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs index 62612a17..d0b41367 100644 --- a/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs +++ b/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs @@ -6,4 +6,5 @@ public class GetBrowseAdsPageResponse public int PageNumber { get; set; } public int PageSize { get; set; } public int TotalPages { get; set; } + public AdSearchCriteriaDto SearchCriteria { get; set; } = new(); } diff --git a/TutorLizard.BusinessLogic/Services/CategoryService.cs b/TutorLizard.BusinessLogic/Services/CategoryService.cs new file mode 100644 index 00000000..64fd065b --- /dev/null +++ b/TutorLizard.BusinessLogic/Services/CategoryService.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using TutorLizard.BusinessLogic.Extensions; +using TutorLizard.BusinessLogic.Interfaces.Data.Repositories; +using TutorLizard.BusinessLogic.Interfaces.Services; +using TutorLizard.BusinessLogic.Models; +using TutorLizard.BusinessLogic.Models.DTOs; + +namespace TutorLizard.BusinessLogic.Services; +public class CategoryService : ICategoryService +{ + private readonly IDbRepository _categoryRepository; + + public CategoryService(IDbRepository categoryRepository) + { + _categoryRepository = categoryRepository; + } + + public async Task> GetAllCategories() + { + return await _categoryRepository + .GetAll() + .Select(category => category.ToDto()) + .ToListAsync(); + } +} diff --git a/TutorLizard.Web/Controllers/BrowseController.cs b/TutorLizard.Web/Controllers/BrowseController.cs index ab5d4257..80fad21e 100644 --- a/TutorLizard.Web/Controllers/BrowseController.cs +++ b/TutorLizard.Web/Controllers/BrowseController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; using TutorLizard.BusinessLogic.Interfaces.Services; +using TutorLizard.BusinessLogic.Models.DTOs; using TutorLizard.BusinessLogic.Models.DTOs.Requests; using TutorLizard.BusinessLogic.Models.DTOs.Responses; using TutorLizard.Web.Interfaces.Services; +using TutorLizard.Web.Models; namespace TutorLizard.Web.Controllers; public class BrowseController : Controller @@ -11,16 +14,19 @@ public class BrowseController : Controller private readonly IBrowseService _browseService; private readonly IUserAuthenticationService _userAuthenticationService; private readonly IUiMessagesService _uiMessagesService; + private readonly ICategoryService _categoryService; private readonly int _pageSize; public BrowseController(IBrowseService browseService, IUserAuthenticationService userAuthenticationService, - IUiMessagesService uiMessagesService) + IUiMessagesService uiMessagesService, + ICategoryService categoryService) { _browseService = browseService; _userAuthenticationService = userAuthenticationService; _uiMessagesService = uiMessagesService; - _pageSize = 10; + _categoryService = categoryService; + _pageSize = 2; } public IActionResult Index() { @@ -32,18 +38,28 @@ public async Task Ads(int id = 1) // TODO customize routing, so that parameter is page, not id int pageNumber = id > 0 ? id : 1; int pageSize = _pageSize > 0 ? _pageSize : 1; - GetBrowseAdsPageRequest request = new(pageNumber, pageSize); - - GetBrowseAdsPageResponse response = await _browseService.GetBrowseAdsPage(request); + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, new()); + return await HandleGetBrowseAdsPageRequest(request, pageNumber); + } - if (response.Success == false) - { - _uiMessagesService.ShowFailureMessage("Wystąpił błąd. Nie udało się się załadować ogłoszeń."); - return RedirectToAction("Index", "Home"); - } + [HttpPost] + public async Task Page([FromForm] GoToAdsPageViewModel model, int id = 1) + { + int pageNumber = id > 0 ? id : 1; + int pageSize = _pageSize > 0 ? _pageSize : 1; + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, model.SearchCriteria); + return await HandleGetBrowseAdsPageRequest(request, pageNumber); + } - return View(response); + [HttpPost] + public async Task Search([FromForm] AdSearchCriteriaDto searchCriteria) + { + int pageNumber = 1; + int pageSize = _pageSize > 0 ? _pageSize : 1; + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + return await HandleGetBrowseAdsPageRequest(request, pageNumber); } + [Authorize] public async Task AdDetails(int id) { @@ -86,4 +102,28 @@ public async Task Schedule() GetUsersScheduleResponse response = await _browseService.GetUsersSchedule(request); return View(response); } + + private async Task HandleGetBrowseAdsPageRequest(GetBrowseAdsPageRequest request, int pageNumber) + { + GetBrowseAdsPageResponse response = await _browseService.GetBrowseAdsPage(request); + + if (response.Success == false) + { + _uiMessagesService.ShowFailureMessage("Wystąpił błąd. Nie udało się się załadować ogłoszeń."); + return RedirectToAction("Index", "Home"); + } + + await AddCategoriesToViewBag(); + + return View(nameof(Ads), response); + } + + private async Task AddCategoriesToViewBag() + { + List categories = await _categoryService.GetAllCategories(); + + ViewBag.Categories = new SelectList(items: categories, + dataValueField: nameof(CategoryDto.Id), + dataTextField: nameof(CategoryDto.Name)); + } } diff --git a/TutorLizard.Web/Controllers/TutorController.cs b/TutorLizard.Web/Controllers/TutorController.cs index fa50acda..66abbb90 100644 --- a/TutorLizard.Web/Controllers/TutorController.cs +++ b/TutorLizard.Web/Controllers/TutorController.cs @@ -200,19 +200,6 @@ public async Task UnacceptScheduleItemRequest(int scheduleItemReq return RedirectToAction(actionName: "AdDetails", controllerName: "Browse", routeValues: new { id = adId }); } - private async Task AddCategoriesToViewBag() - { - List categories = await _categoryRepository - .GetAll() - .ToListAsync(); - - List categoryDtos = categories.Select(c => c.ToDto()).ToList(); - - ViewBag.Categories = new SelectList(items: categoryDtos, - dataValueField: nameof(CategoryDto.Id), - dataTextField: nameof(CategoryDto.Name)); - } - public async Task ViewPendingAdRequests() { try @@ -329,4 +316,17 @@ public async Task TutorsAdsList() return RedirectToAction("Error", "Home"); } } + + private async Task AddCategoriesToViewBag() + { + List categories = await _categoryRepository + .GetAll() + .ToListAsync(); + + List categoryDtos = categories.Select(c => c.ToDto()).ToList(); + + ViewBag.Categories = new SelectList(items: categoryDtos, + dataValueField: nameof(CategoryDto.Id), + dataTextField: nameof(CategoryDto.Name)); + } } diff --git a/TutorLizard.Web/Models/GoToAdsPageViewModel.cs b/TutorLizard.Web/Models/GoToAdsPageViewModel.cs new file mode 100644 index 00000000..4e445e9d --- /dev/null +++ b/TutorLizard.Web/Models/GoToAdsPageViewModel.cs @@ -0,0 +1,22 @@ +using TutorLizard.BusinessLogic.Models.DTOs; + +namespace TutorLizard.Web.Models; + +public class GoToAdsPageViewModel +{ + public GoToAdsPageViewModel(int pageNumber, string text, AdSearchCriteriaDto searchCriteria) + { + SearchCriteria = searchCriteria ?? throw new ArgumentNullException(nameof(searchCriteria)); + PageNumber = pageNumber; + Text = text ?? throw new ArgumentNullException(nameof(text)); + } + + public GoToAdsPageViewModel() + { + + } + + public AdSearchCriteriaDto SearchCriteria { get; set; } = new(); + public int PageNumber { get; set; } + public string Text { get; set; } = ""; +} diff --git a/TutorLizard.Web/Program.cs b/TutorLizard.Web/Program.cs index eee00b52..a14eac69 100644 --- a/TutorLizard.Web/Program.cs +++ b/TutorLizard.Web/Program.cs @@ -28,6 +28,7 @@ .ValidateDataAnnotations(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddAuthentication("CookieAuth") diff --git a/TutorLizard.Web/Strings/PartialNames.cs b/TutorLizard.Web/Strings/PartialNames.cs index ceeaa55b..6242c47c 100644 --- a/TutorLizard.Web/Strings/PartialNames.cs +++ b/TutorLizard.Web/Strings/PartialNames.cs @@ -7,4 +7,6 @@ public static class PartialNames public const string AdRequest = "_AdRequests"; public const string PendingAdRequestsListItem = "_PendingAdRequestsListItem"; public const string TutorAllAdRequestsListItem = "_TutorAllAdRequestsListItem"; + public const string AdSearchCriteria = "_AdSearchCriteria"; + public const string GoToAdsPage = "_GoToAdsPage"; } diff --git a/TutorLizard.Web/Views/Browse/Ads.cshtml b/TutorLizard.Web/Views/Browse/Ads.cshtml index 7b4663b1..697accf8 100644 --- a/TutorLizard.Web/Views/Browse/Ads.cshtml +++ b/TutorLizard.Web/Views/Browse/Ads.cshtml @@ -6,6 +6,11 @@

Ogłoszenia

+
+

Wyszukiwanie ogłoszeń

+ + + @foreach (var ad in Model.Ads) { @@ -16,7 +21,8 @@
@if(Model.PageNumber > 1) { - Poprzednia strona + }
@@ -27,7 +33,8 @@
@if(Model.TotalPages > Model.PageNumber) { - Następna strona + }
diff --git a/TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml b/TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml new file mode 100644 index 00000000..80b8506e --- /dev/null +++ b/TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml @@ -0,0 +1,60 @@ +@model AdSearchCriteriaDto + +
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + @if (ViewBag.Categories is not null) + { + + } + +
+ +
+ +
+
+ + +
+
+
\ No newline at end of file diff --git a/TutorLizard.Web/Views/Shared/_GoToAdsPage.cshtml b/TutorLizard.Web/Views/Shared/_GoToAdsPage.cshtml new file mode 100644 index 00000000..81f7f7a3 --- /dev/null +++ b/TutorLizard.Web/Views/Shared/_GoToAdsPage.cshtml @@ -0,0 +1,17 @@ +@model GoToAdsPageViewModel + +
+ + + + + + + + + + + + + +
\ No newline at end of file From 431af05f43ab14073372bbe309c91db61f023805 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Mon, 17 Jun 2024 22:28:38 +0200 Subject: [PATCH 02/12] Add extension methods for AdSearchCriteriaDto with tests The methods are to convert this DTO to and from a Base64 string. --- .../AdSearchCriteriaDtoExtensionsTests.cs | 36 +++++++++++++++++++ TutorLizard.Web/Extensions/DtoExtensions.cs | 26 ++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs create mode 100644 TutorLizard.Web/Extensions/DtoExtensions.cs diff --git a/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs b/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs new file mode 100644 index 00000000..45587cde --- /dev/null +++ b/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs @@ -0,0 +1,36 @@ +using AutoFixture; +using TutorLizard.BusinessLogic.Models.DTOs; +using TutorLizard.Web.Extensions; + +namespace TutorLizard.Web.Tests.Extensions; +public class AdSearchCriteriaDtoExtensionsTests +{ + Fixture _fixture = new(); + + [Fact] + public void ToAdSearchCriteriaDto_WhenCalledOnResultOfToBase64String_ShouldCorrectlyDeserialize() + { + // Arrange + AdSearchCriteriaDto expected = _fixture.Create(); + string serialized = expected.ToBase64String(); + + // Act + AdSearchCriteriaDto? actual = serialized.ToAdSearchCriteriaDto(); + + // Assert + Assert.NotNull(actual); + Assert.Equivalent(expected, actual); + } + + [Fact] + public void ToSearchCriteriaDto_WhenCalledOnRandomString_ShouldReturnNull() + { + // Arrange + string input = _fixture.Create(); + + // Act + AdSearchCriteriaDto? actual = input.ToAdSearchCriteriaDto(); + + Assert.Null(actual); + } +} diff --git a/TutorLizard.Web/Extensions/DtoExtensions.cs b/TutorLizard.Web/Extensions/DtoExtensions.cs new file mode 100644 index 00000000..bf726f8c --- /dev/null +++ b/TutorLizard.Web/Extensions/DtoExtensions.cs @@ -0,0 +1,26 @@ +using System.Text.Json; +using TutorLizard.BusinessLogic.Models.DTOs; + +namespace TutorLizard.Web.Extensions; + +public static class DtoExtensions +{ + public static string ToBase64String(this AdSearchCriteriaDto searchCriteria) + { + byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(searchCriteria); + return Convert.ToBase64String(bytes); + } + + public static AdSearchCriteriaDto? ToAdSearchCriteriaDto(this string base64String) + { + try + { + byte[] bytes = Convert.FromBase64String(base64String); + return JsonSerializer.Deserialize(bytes); + } + catch + { + return null; + } + } +} From 931cc3059a0ed2d7a2bb0392ce01dc40d93e6bf9 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Mon, 17 Jun 2024 22:30:25 +0200 Subject: [PATCH 03/12] Add request and response DTOs for GetGategories() --- .../Controllers/Browse/BrowseControllerAdsTests.cs | 9 +++++++-- .../Interfaces/Services/ICategoryService.cs | 5 +++-- .../Models/DTOs/Requests/GetCategoriesRequest.cs | 5 +++++ .../Models/DTOs/Responses/GetCategoriesResponse.cs | 6 ++++++ .../Services/CategoryService.cs | 12 ++++++++++-- TutorLizard.Web/Controllers/BrowseController.cs | 5 +++-- 6 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 TutorLizard.BusinessLogic/Models/DTOs/Requests/GetCategoriesRequest.cs create mode 100644 TutorLizard.BusinessLogic/Models/DTOs/Responses/GetCategoriesResponse.cs diff --git a/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerAdsTests.cs b/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerAdsTests.cs index 9cd51ad0..9855d1a6 100644 --- a/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerAdsTests.cs +++ b/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerAdsTests.cs @@ -125,7 +125,12 @@ private GetBrowseAdsPageResponse CreateGetBrowseAdsPageResponse(bool success) private void SetupMockCategoryServiceGetAll(List categories) { MockCategoryService - .Setup(x => x.GetAllCategories()) - .Returns(Task.FromResult(categories)); + .Setup(x => x.GetCategories(It.IsAny())) + .Returns(Task.FromResult( + new GetCategoriesResponse() + { + Success = true, + Categories = categories + })); } } diff --git a/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs b/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs index a7910b5f..547bee7e 100644 --- a/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs +++ b/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs @@ -1,7 +1,8 @@ -using TutorLizard.BusinessLogic.Models.DTOs; +using TutorLizard.BusinessLogic.Models.DTOs.Requests; +using TutorLizard.BusinessLogic.Models.DTOs.Responses; namespace TutorLizard.BusinessLogic.Interfaces.Services; public interface ICategoryService { - Task> GetAllCategories(); + Task GetCategories(GetCategoriesRequest request); } \ No newline at end of file diff --git a/TutorLizard.BusinessLogic/Models/DTOs/Requests/GetCategoriesRequest.cs b/TutorLizard.BusinessLogic/Models/DTOs/Requests/GetCategoriesRequest.cs new file mode 100644 index 00000000..8a17f5f8 --- /dev/null +++ b/TutorLizard.BusinessLogic/Models/DTOs/Requests/GetCategoriesRequest.cs @@ -0,0 +1,5 @@ +namespace TutorLizard.BusinessLogic.Models.DTOs.Requests; + +public class GetCategoriesRequest +{ +} \ No newline at end of file diff --git a/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetCategoriesResponse.cs b/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetCategoriesResponse.cs new file mode 100644 index 00000000..42a84ed3 --- /dev/null +++ b/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetCategoriesResponse.cs @@ -0,0 +1,6 @@ +namespace TutorLizard.BusinessLogic.Models.DTOs.Responses; +public class GetCategoriesResponse +{ + public bool Success { get; set; } + public List Categories { get; set; } = new(); +} diff --git a/TutorLizard.BusinessLogic/Services/CategoryService.cs b/TutorLizard.BusinessLogic/Services/CategoryService.cs index 64fd065b..5f807e62 100644 --- a/TutorLizard.BusinessLogic/Services/CategoryService.cs +++ b/TutorLizard.BusinessLogic/Services/CategoryService.cs @@ -4,6 +4,8 @@ using TutorLizard.BusinessLogic.Interfaces.Services; using TutorLizard.BusinessLogic.Models; using TutorLizard.BusinessLogic.Models.DTOs; +using TutorLizard.BusinessLogic.Models.DTOs.Requests; +using TutorLizard.BusinessLogic.Models.DTOs.Responses; namespace TutorLizard.BusinessLogic.Services; public class CategoryService : ICategoryService @@ -15,11 +17,17 @@ public CategoryService(IDbRepository categoryRepository) _categoryRepository = categoryRepository; } - public async Task> GetAllCategories() + public async Task GetCategories(GetCategoriesRequest request) { - return await _categoryRepository + List categories = await _categoryRepository .GetAll() .Select(category => category.ToDto()) .ToListAsync(); + + return new GetCategoriesResponse() + { + Success = true, + Categories = categories + }; } } diff --git a/TutorLizard.Web/Controllers/BrowseController.cs b/TutorLizard.Web/Controllers/BrowseController.cs index 80fad21e..faf2200f 100644 --- a/TutorLizard.Web/Controllers/BrowseController.cs +++ b/TutorLizard.Web/Controllers/BrowseController.cs @@ -120,9 +120,10 @@ private async Task HandleGetBrowseAdsPageRequest(GetBrowseAdsPage private async Task AddCategoriesToViewBag() { - List categories = await _categoryService.GetAllCategories(); + GetCategoriesRequest request = new(); + GetCategoriesResponse response = await _categoryService.GetCategories(request); - ViewBag.Categories = new SelectList(items: categories, + ViewBag.Categories = new SelectList(items: response.Categories, dataValueField: nameof(CategoryDto.Id), dataTextField: nameof(CategoryDto.Name)); } From 9281303831020b98fa1a4b7e5aba65441e60cc4d Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Mon, 17 Jun 2024 23:20:33 +0200 Subject: [PATCH 04/12] Change the way search criteria are passed in Browse/Ads --- .../Models/DTOs/AdSearchCriteriaDto.cs | 7 ++++ .../Services/BrowseService.cs | 3 +- .../Controllers/BrowseController.cs | 39 ++++++++++++------- TutorLizard.Web/Views/Browse/Ads.cshtml | 24 +++++++++--- .../Views/Shared/_GoToAdsPage.cshtml | 17 -------- 5 files changed, 52 insertions(+), 38 deletions(-) delete mode 100644 TutorLizard.Web/Views/Shared/_GoToAdsPage.cshtml diff --git a/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs b/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs index 31ae3812..76dc174a 100644 --- a/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs +++ b/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs @@ -8,6 +8,13 @@ public class AdSearchCriteriaDto public bool SearchByIsRemote { get; set; } public bool SearchByCategoryId { get; set; } + public bool AnySearch => SearchByText || + SearchByPriceMin || + SearchByPriceMax || + SearchByLocation || + SearchByIsRemote || + SearchByCategoryId; + public string Text { get; set; } = ""; public decimal PriceMin { get; set; } = 0; public decimal PriceMax { get; set; } = 0; diff --git a/TutorLizard.BusinessLogic/Services/BrowseService.cs b/TutorLizard.BusinessLogic/Services/BrowseService.cs index 9bf8f721..5c24d985 100644 --- a/TutorLizard.BusinessLogic/Services/BrowseService.cs +++ b/TutorLizard.BusinessLogic/Services/BrowseService.cs @@ -69,7 +69,8 @@ public async Task GetBrowseAdsPage(GetBrowseAdsPageReq Ads = ads, PageNumber = request.PageNumber, PageSize = request.PageSize, - TotalPages = totalPages + TotalPages = totalPages, + SearchCriteria = request.SearchCriteria }; return response; diff --git a/TutorLizard.Web/Controllers/BrowseController.cs b/TutorLizard.Web/Controllers/BrowseController.cs index faf2200f..f987db23 100644 --- a/TutorLizard.Web/Controllers/BrowseController.cs +++ b/TutorLizard.Web/Controllers/BrowseController.cs @@ -7,6 +7,7 @@ using TutorLizard.BusinessLogic.Models.DTOs.Responses; using TutorLizard.Web.Interfaces.Services; using TutorLizard.Web.Models; +using TutorLizard.Web.Extensions; namespace TutorLizard.Web.Controllers; public class BrowseController : Controller @@ -33,31 +34,39 @@ public IActionResult Index() return RedirectToAction(nameof(Ads)); } - public async Task Ads(int id = 1) + public async Task Ads(int id = 1, [FromQuery] string? search = "") { // TODO customize routing, so that parameter is page, not id int pageNumber = id > 0 ? id : 1; int pageSize = _pageSize > 0 ? _pageSize : 1; - GetBrowseAdsPageRequest request = new(pageNumber, pageSize, new()); - return await HandleGetBrowseAdsPageRequest(request, pageNumber); - } - [HttpPost] - public async Task Page([FromForm] GoToAdsPageViewModel model, int id = 1) - { - int pageNumber = id > 0 ? id : 1; - int pageSize = _pageSize > 0 ? _pageSize : 1; - GetBrowseAdsPageRequest request = new(pageNumber, pageSize, model.SearchCriteria); - return await HandleGetBrowseAdsPageRequest(request, pageNumber); + AdSearchCriteriaDto? searchCriteria = search?.ToAdSearchCriteriaDto(); + searchCriteria ??= new(); + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + GetBrowseAdsPageResponse response = await _browseService.GetBrowseAdsPage(request); + + if (response.Success == false) + { + _uiMessagesService.ShowFailureMessage("Wystąpił błąd. Nie udało się się załadować ogłoszeń."); + return RedirectToAction("Index", "Home"); + } + + await AddCategoriesToViewBag(); + + return View(nameof(Ads), response); } [HttpPost] - public async Task Search([FromForm] AdSearchCriteriaDto searchCriteria) + public IActionResult Search([FromForm] AdSearchCriteriaDto searchCriteria) { int pageNumber = 1; - int pageSize = _pageSize > 0 ? _pageSize : 1; - GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); - return await HandleGetBrowseAdsPageRequest(request, pageNumber); + string search = searchCriteria.AnySearch ? + searchCriteria.ToBase64String() : + ""; + + return RedirectToAction(nameof(Ads), "Browse", new { id = pageNumber, search }); } [Authorize] diff --git a/TutorLizard.Web/Views/Browse/Ads.cshtml b/TutorLizard.Web/Views/Browse/Ads.cshtml index 697accf8..22aa9ec3 100644 --- a/TutorLizard.Web/Views/Browse/Ads.cshtml +++ b/TutorLizard.Web/Views/Browse/Ads.cshtml @@ -1,4 +1,5 @@ -@model GetBrowseAdsPageResponse +@using TutorLizard.Web.Extensions +@model GetBrowseAdsPageResponse @{ ViewData["Title"] = "Ogłoszenia"; @@ -19,10 +20,17 @@
+ @if(Model.PageNumber > 1) { - + @if (Model.SearchCriteria.AnySearch) + { + Poprzednia strona + } + else + { + Poprzednia strona + } }
@@ -33,8 +41,14 @@
@if(Model.TotalPages > Model.PageNumber) { - + @if(Model.SearchCriteria.AnySearch) + { + Następna strona + } + else + { + Następna strona + } }
diff --git a/TutorLizard.Web/Views/Shared/_GoToAdsPage.cshtml b/TutorLizard.Web/Views/Shared/_GoToAdsPage.cshtml deleted file mode 100644 index 81f7f7a3..00000000 --- a/TutorLizard.Web/Views/Shared/_GoToAdsPage.cshtml +++ /dev/null @@ -1,17 +0,0 @@ -@model GoToAdsPageViewModel - -
- - - - - - - - - - - - - -
\ No newline at end of file From 78a821aa2613ba9b760da86cd1ef638b6ea757cf Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Tue, 18 Jun 2024 13:37:37 +0200 Subject: [PATCH 05/12] Add AdSearchCriteriaViewModel, tests for search criteria I split responsibilities of Dto and ViewModel. Dto is used to communicate with BrowseService, ViewModel is used as model for search criteria form. This way, a more concise Dto type can be encoded as a search query string. --- .../Models/Dtos/AdSearchCriteriaDtoTests.cs | 59 +++++++ .../AdSearchCriteriaDtoExtensionsTests.cs | 164 ++++++++++++++++++ .../Models/AdSearchCriteriaViewModelTests.cs | 60 +++++++ .../Models/DTOs/AdSearchCriteriaDto.cs | 36 ++-- .../Controllers/BrowseController.cs | 6 +- .../AdSearchCriteriaDtoExtensions.cs | 69 ++++++++ TutorLizard.Web/Extensions/DtoExtensions.cs | 26 --- .../Models/AdSearchCriteriaViewModel.cs | 24 +++ .../Models/GoToAdsPageViewModel.cs | 22 --- TutorLizard.Web/Views/Browse/Ads.cshtml | 2 +- .../Views/Shared/_AdSearchCriteria.cshtml | 2 +- 11 files changed, 397 insertions(+), 73 deletions(-) create mode 100644 Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs create mode 100644 Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs create mode 100644 TutorLizard.Web/Extensions/AdSearchCriteriaDtoExtensions.cs delete mode 100644 TutorLizard.Web/Extensions/DtoExtensions.cs create mode 100644 TutorLizard.Web/Models/AdSearchCriteriaViewModel.cs delete mode 100644 TutorLizard.Web/Models/GoToAdsPageViewModel.cs diff --git a/Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs b/Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs new file mode 100644 index 00000000..37d797d8 --- /dev/null +++ b/Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs @@ -0,0 +1,59 @@ +using TutorLizard.BusinessLogic.Models.DTOs; + +namespace TutorLizard.BusinessLogic.Tests.Models.Dtos; +public class AdSearchCriteriaDtoTests +{ + [Theory] + [InlineData("", null, null, null, null, null)] + [InlineData(null, 0, null, null, null, null)] + [InlineData(null, null, 0, null, null, null)] + [InlineData(null, null, null, "", null, null)] + [InlineData(null, null, null, null, false, null)] + [InlineData(null, null, null, null, null, 1)] + [InlineData("", 0, 0, "", false, 1)] + public void AnySearch_WhenAnySearchValueIsNotNull_ShouldReturnTrue(string? text, + int? priceMin, + int? priceMax, + string? location, + bool? isRemote, + int? categoryId) + { + // Arrange + bool expected = true; + + // Act + AdSearchCriteriaDto actual = new() + { + Text = text, + PriceMin = (decimal?) priceMin, + PriceMax = (decimal?) priceMax, + Location = location, + IsRemote = isRemote, + CategoryId = categoryId, + }; + + // Assert + Assert.Equal(expected, actual.AnySearch); + } + + [Fact] + public void AnySearch_WhenAllSearchValuesAreNull_ShouldReturnFalse() + { + // Arrange + bool expected = false; + + // Act + AdSearchCriteriaDto actual = new() + { + Text = null, + PriceMin = null, + PriceMax = null, + Location = null, + IsRemote = null, + CategoryId = null, + }; + + // Assert + Assert.Equal(expected, actual.AnySearch); + } +} diff --git a/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs b/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs index 45587cde..be9700cf 100644 --- a/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs +++ b/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs @@ -1,12 +1,59 @@ using AutoFixture; using TutorLizard.BusinessLogic.Models.DTOs; using TutorLizard.Web.Extensions; +using TutorLizard.Web.Models; namespace TutorLizard.Web.Tests.Extensions; public class AdSearchCriteriaDtoExtensionsTests { Fixture _fixture = new(); + [Fact] + public void ToBase64String_WhenAnySearchIsTrue_ShouldReturnNotEmptyString() + { + // Arrange + AdSearchCriteriaDto dto = _fixture + .Build() + .With(dto => dto.Text, "") + .With(dto => dto.PriceMin, 0) + .With(dto => dto.PriceMax, 0) + .With(dto => dto.Location, "") + .With(dto => dto.IsRemote, false) + .With(dto => dto.CategoryId, 1) + .Create(); + + // Act + string actual = dto.ToBase64String(); + + // Assert + Assert.NotNull(actual); + Assert.False(String.IsNullOrEmpty(actual)); + Assert.False(String.IsNullOrWhiteSpace(actual)); + } + + [Fact] + public void ToBase64String_WhenAnySearchIsFalse_ShouldReturnEmptyString() + { + // Arrange + AdSearchCriteriaDto dto = _fixture + .Build() + .With(dto => dto.Text, (string?)null) + .With(dto => dto.PriceMin, (decimal?)null) + .With(dto => dto.PriceMax, (decimal?)null) + .With(dto => dto.Location, (string?)null) + .With(dto => dto.IsRemote, (bool?)null) + .With(dto => dto.CategoryId, (int?)null) + .Create(); + + // Act + string actual = dto.ToBase64String(); + + // Assert + Assert.NotNull(actual); + Assert.True(String.IsNullOrEmpty(actual)); + Assert.True(String.IsNullOrWhiteSpace(actual)); + } + [Fact] public void ToAdSearchCriteriaDto_WhenCalledOnResultOfToBase64String_ShouldCorrectlyDeserialize() { @@ -33,4 +80,121 @@ public void ToSearchCriteriaDto_WhenCalledOnRandomString_ShouldReturnNull() Assert.Null(actual); } + + [Fact] + public void ToDto_WhenSearchByPropertiesAreTrue_SetsSearchValues() + { + // Arrange + AdSearchCriteriaViewModel viewModel = _fixture + .Build() + .With(vm => vm.SearchByText, true) + .With(vm => vm.SearchByPriceMin, true) + .With(vm => vm.SearchByPriceMax, true) + .With(vm => vm.SearchByLocation, true) + .With(vm => vm.SearchByIsRemote, true) + .With(vm => vm.SearchByCategoryId, true) + .Create(); + + // Act + AdSearchCriteriaDto dto = viewModel.ToDto(); + + // Assert + Assert.NotNull(dto); + Assert.Equal(viewModel.Text, dto.Text); + Assert.Equal(viewModel.PriceMin, dto.PriceMin); + Assert.Equal(viewModel.PriceMax, dto.PriceMax); + Assert.Equal(viewModel.Location, dto.Location); + Assert.Equal(viewModel.IsRemote, dto.IsRemote); + Assert.Equal(viewModel.CategoryId, dto.CategoryId); + } + + [Fact] + public void ToDto_WhenSearchByPropertiesAreFalse_SetsNullSearchValues() + { + // Arrange + AdSearchCriteriaViewModel viewModel = _fixture + .Build() + .With(vm => vm.SearchByText, false) + .With(vm => vm.SearchByPriceMin, false) + .With(vm => vm.SearchByPriceMax, false) + .With(vm => vm.SearchByLocation, false) + .With(vm => vm.SearchByIsRemote, false) + .With(vm => vm.SearchByCategoryId, false) + .Create(); + + // Act + AdSearchCriteriaDto dto = viewModel.ToDto(); + + // Assert + Assert.NotNull(dto); + Assert.Null(dto.Text); + Assert.Null(dto.PriceMin); + Assert.Null(dto.PriceMax); + Assert.Null(dto.Location); + Assert.Null(dto.IsRemote); + Assert.Null(dto.CategoryId); + } + + + [Fact] + public void ToViewModel_WhenSearchValueIsNotNull_ShouldCorrectlySetProperties() + { + // Arrange + AdSearchCriteriaDto expected = _fixture.Create(); + + // Act + AdSearchCriteriaViewModel actual = expected.ToViewModel(); + + // Assert + Assert.NotNull(actual); + + Assert.True(actual.SearchByText); + Assert.True(actual.SearchByPriceMin); + Assert.True(actual.SearchByPriceMax); + Assert.True(actual.SearchByLocation); + Assert.True(actual.SearchByIsRemote); + Assert.True(actual.SearchByCategoryId); + + Assert.Equal(expected.Text, actual.Text); + Assert.Equal(expected.PriceMin, actual.PriceMin); + Assert.Equal(expected.PriceMax, actual.PriceMax); + Assert.Equal(expected.Location, actual.Location); + Assert.Equal(expected.IsRemote, actual.IsRemote); + Assert.Equal(expected.CategoryId, actual.CategoryId); + } + + [Fact] + public void ToViewModel_WhenSearchValueIsNull_ShouldCorrectlySetProperties() + { + // Arrange + AdSearchCriteriaDto dto = _fixture + .Build() + .With(dto => dto.Text, (string?)null) + .With(dto => dto.PriceMin, (decimal?)null) + .With(dto => dto.PriceMax, (decimal?)null) + .With(dto => dto.Location, (string?)null) + .With(dto => dto.IsRemote, (bool?)null) + .With(dto => dto.CategoryId, (int?)null) + .Create(); + + // Act + AdSearchCriteriaViewModel actual = dto.ToViewModel(); + + // Assert + Assert.NotNull(actual); + + Assert.False(actual.SearchByText); + Assert.False(actual.SearchByPriceMin); + Assert.False(actual.SearchByPriceMax); + Assert.False(actual.SearchByLocation); + Assert.False(actual.SearchByIsRemote); + Assert.False(actual.SearchByCategoryId); + + Assert.Equal("", actual.Text); + Assert.Equal(0, actual.PriceMin); + Assert.Equal(0, actual.PriceMax); + Assert.Equal("", actual.Location); + Assert.Equal(false, actual.IsRemote); + Assert.Equal(1, actual.CategoryId); + } } diff --git a/Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs b/Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs new file mode 100644 index 00000000..33d10927 --- /dev/null +++ b/Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs @@ -0,0 +1,60 @@ +using TutorLizard.BusinessLogic.Models.DTOs; +using TutorLizard.Web.Models; + +namespace TutorLizard.Web.Tests.Models; +public class AdSearchCriteriaViewModelTests +{ + [Theory] + [InlineData(true, false, false, false, false, false)] + [InlineData(false, true, false, false, false, false)] + [InlineData(false, false, true, false, false, false)] + [InlineData(false, false, false, true, false, false)] + [InlineData(false, false, false, false, true, false)] + [InlineData(false, false, false, false, false, true)] + [InlineData(true, true, true, true, true, true)] + public void AnySearch_WhenAnySerchByPropertyIsTrue_ShouldReturnTrue(bool searchByText, + bool searchByPriceMin, + bool searchByPriceMax, + bool searchByLocation, + bool searchByIsRemote, + bool searchByCategoryId) + { + // Arrange + bool expected = true; + + // Act + AdSearchCriteriaViewModel actual = new() + { + SearchByText = searchByText, + SearchByPriceMin = searchByPriceMin, + SearchByPriceMax = searchByPriceMax, + SearchByLocation = searchByLocation, + SearchByIsRemote = searchByIsRemote, + SearchByCategoryId = searchByCategoryId, + }; + + // Assert + Assert.Equal(expected, actual.AnySearch); + } + + [Fact] + public void AnySearch_WhenAllSerchByPropertiesAreFalse_ShouldReturnFalse() + { + // Arrange + bool expected = false; + + // Act + AdSearchCriteriaViewModel actual = new() + { + SearchByText = false, + SearchByPriceMin = false, + SearchByPriceMax = false, + SearchByLocation = false, + SearchByIsRemote = false, + SearchByCategoryId = false, + }; + + // Assert + Assert.Equal(expected, actual.AnySearch); + } +} diff --git a/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs b/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs index 76dc174a..f710d128 100644 --- a/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs +++ b/TutorLizard.BusinessLogic/Models/DTOs/AdSearchCriteriaDto.cs @@ -1,24 +1,20 @@ -namespace TutorLizard.BusinessLogic.Models.DTOs; +using System.Text.Json.Serialization; + +namespace TutorLizard.BusinessLogic.Models.DTOs; public class AdSearchCriteriaDto { - public bool SearchByText { get; set; } - public bool SearchByPriceMin { get; set; } - public bool SearchByPriceMax { get; set; } - public bool SearchByLocation { get; set; } - public bool SearchByIsRemote { get; set; } - public bool SearchByCategoryId { get; set; } - - public bool AnySearch => SearchByText || - SearchByPriceMin || - SearchByPriceMax || - SearchByLocation || - SearchByIsRemote || - SearchByCategoryId; + public string? Text { get; set; } + public decimal? PriceMin { get; set; } + public decimal? PriceMax { get; set; } + public string? Location { get; set; } + public bool? IsRemote { get; set; } + public int? CategoryId { get; set; } - public string Text { get; set; } = ""; - public decimal PriceMin { get; set; } = 0; - public decimal PriceMax { get; set; } = 0; - public string Location { get; set; } = ""; - public bool IsRemote { get; set; } = false; - public int CategoryId { get; set; } = 1; + [JsonIgnore] + public bool AnySearch => Text is not null || + PriceMin is not null || + PriceMax is not null || + Location is not null || + IsRemote is not null || + CategoryId is not null; } diff --git a/TutorLizard.Web/Controllers/BrowseController.cs b/TutorLizard.Web/Controllers/BrowseController.cs index f987db23..1bfc9bab 100644 --- a/TutorLizard.Web/Controllers/BrowseController.cs +++ b/TutorLizard.Web/Controllers/BrowseController.cs @@ -27,7 +27,7 @@ public class BrowseController : Controller _userAuthenticationService = userAuthenticationService; _uiMessagesService = uiMessagesService; _categoryService = categoryService; - _pageSize = 2; + _pageSize = 10; } public IActionResult Index() { @@ -59,11 +59,11 @@ public async Task Ads(int id = 1, [FromQuery] string? search = "" } [HttpPost] - public IActionResult Search([FromForm] AdSearchCriteriaDto searchCriteria) + public IActionResult Search([FromForm] AdSearchCriteriaViewModel searchCriteria) { int pageNumber = 1; string search = searchCriteria.AnySearch ? - searchCriteria.ToBase64String() : + searchCriteria.ToDto().ToBase64String() : ""; return RedirectToAction(nameof(Ads), "Browse", new { id = pageNumber, search }); diff --git a/TutorLizard.Web/Extensions/AdSearchCriteriaDtoExtensions.cs b/TutorLizard.Web/Extensions/AdSearchCriteriaDtoExtensions.cs new file mode 100644 index 00000000..f14a6081 --- /dev/null +++ b/TutorLizard.Web/Extensions/AdSearchCriteriaDtoExtensions.cs @@ -0,0 +1,69 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using TutorLizard.BusinessLogic.Models.DTOs; +using TutorLizard.Web.Models; + +namespace TutorLizard.Web.Extensions; + +public static class AdSearchCriteriaDtoExtensions +{ + static JsonSerializerOptions _serializerOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }; + + public static string ToBase64String(this AdSearchCriteriaDto searchCriteria) + { + if (searchCriteria.AnySearch == false) + { + return ""; + } + byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(searchCriteria, _serializerOptions); + return Convert.ToBase64String(bytes); + } + + public static AdSearchCriteriaDto? ToAdSearchCriteriaDto(this string base64String) + { + try + { + byte[] bytes = Convert.FromBase64String(base64String); + return JsonSerializer.Deserialize(bytes, _serializerOptions); + } + catch + { + return null; + } + } + + public static AdSearchCriteriaDto ToDto(this AdSearchCriteriaViewModel viewModel) + { + return new AdSearchCriteriaDto() + { + Text = viewModel.SearchByText ? viewModel.Text : null, + PriceMin = viewModel.SearchByPriceMin ? viewModel.PriceMin : null, + PriceMax = viewModel.SearchByPriceMax ? viewModel.PriceMax : null, + Location = viewModel.SearchByLocation ? viewModel.Location : null, + IsRemote = viewModel.SearchByIsRemote ? viewModel.IsRemote : null, + CategoryId = viewModel.SearchByCategoryId ? viewModel.CategoryId : null, + }; + } + + public static AdSearchCriteriaViewModel ToViewModel(this AdSearchCriteriaDto dto) + { + return new AdSearchCriteriaViewModel() + { + SearchByText = String.IsNullOrEmpty(dto.Text) == false, + SearchByPriceMin = dto.PriceMin is not null, + SearchByPriceMax = dto.PriceMax is not null, + SearchByLocation = String.IsNullOrEmpty(dto.Location) == false, + SearchByIsRemote = dto.IsRemote is not null, + SearchByCategoryId = dto.CategoryId is not null, + Text = dto.Text ?? "", + PriceMin = dto.PriceMin ?? 0, + PriceMax = dto.PriceMax ?? 0, + Location = dto.Location ?? "", + IsRemote = dto.IsRemote ?? false, + CategoryId = dto.CategoryId ?? 1, + }; + } +} diff --git a/TutorLizard.Web/Extensions/DtoExtensions.cs b/TutorLizard.Web/Extensions/DtoExtensions.cs deleted file mode 100644 index bf726f8c..00000000 --- a/TutorLizard.Web/Extensions/DtoExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Text.Json; -using TutorLizard.BusinessLogic.Models.DTOs; - -namespace TutorLizard.Web.Extensions; - -public static class DtoExtensions -{ - public static string ToBase64String(this AdSearchCriteriaDto searchCriteria) - { - byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(searchCriteria); - return Convert.ToBase64String(bytes); - } - - public static AdSearchCriteriaDto? ToAdSearchCriteriaDto(this string base64String) - { - try - { - byte[] bytes = Convert.FromBase64String(base64String); - return JsonSerializer.Deserialize(bytes); - } - catch - { - return null; - } - } -} diff --git a/TutorLizard.Web/Models/AdSearchCriteriaViewModel.cs b/TutorLizard.Web/Models/AdSearchCriteriaViewModel.cs new file mode 100644 index 00000000..2e36bbd3 --- /dev/null +++ b/TutorLizard.Web/Models/AdSearchCriteriaViewModel.cs @@ -0,0 +1,24 @@ +namespace TutorLizard.Web.Models; +public class AdSearchCriteriaViewModel +{ + public bool SearchByText { get; set; } + public bool SearchByPriceMin { get; set; } + public bool SearchByPriceMax { get; set; } + public bool SearchByLocation { get; set; } + public bool SearchByIsRemote { get; set; } + public bool SearchByCategoryId { get; set; } + + public bool AnySearch => SearchByText || + SearchByPriceMin || + SearchByPriceMax || + SearchByLocation || + SearchByIsRemote || + SearchByCategoryId; + + public string Text { get; set; } = ""; + public decimal PriceMin { get; set; } = 0; + public decimal PriceMax { get; set; } = 0; + public string Location { get; set; } = ""; + public bool IsRemote { get; set; } = false; + public int CategoryId { get; set; } = 1; +} diff --git a/TutorLizard.Web/Models/GoToAdsPageViewModel.cs b/TutorLizard.Web/Models/GoToAdsPageViewModel.cs deleted file mode 100644 index 4e445e9d..00000000 --- a/TutorLizard.Web/Models/GoToAdsPageViewModel.cs +++ /dev/null @@ -1,22 +0,0 @@ -using TutorLizard.BusinessLogic.Models.DTOs; - -namespace TutorLizard.Web.Models; - -public class GoToAdsPageViewModel -{ - public GoToAdsPageViewModel(int pageNumber, string text, AdSearchCriteriaDto searchCriteria) - { - SearchCriteria = searchCriteria ?? throw new ArgumentNullException(nameof(searchCriteria)); - PageNumber = pageNumber; - Text = text ?? throw new ArgumentNullException(nameof(text)); - } - - public GoToAdsPageViewModel() - { - - } - - public AdSearchCriteriaDto SearchCriteria { get; set; } = new(); - public int PageNumber { get; set; } - public string Text { get; set; } = ""; -} diff --git a/TutorLizard.Web/Views/Browse/Ads.cshtml b/TutorLizard.Web/Views/Browse/Ads.cshtml index 22aa9ec3..833dc7d3 100644 --- a/TutorLizard.Web/Views/Browse/Ads.cshtml +++ b/TutorLizard.Web/Views/Browse/Ads.cshtml @@ -9,7 +9,7 @@

Wyszukiwanie ogłoszeń

- + @foreach (var ad in Model.Ads) diff --git a/TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml b/TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml index 80b8506e..0bc5debd 100644 --- a/TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml +++ b/TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml @@ -1,4 +1,4 @@ -@model AdSearchCriteriaDto +@model AdSearchCriteriaViewModel
From 0bd161c949176838a77709a4c3e9ef3806364552 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Tue, 18 Jun 2024 14:11:35 +0200 Subject: [PATCH 06/12] Add BrowseControllerSearchTests --- .../Browse/BrowseControllerSearchTests.cs | 26 +++++++++++++++++++ .../Controllers/BrowseController.cs | 7 ++--- 2 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerSearchTests.cs diff --git a/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerSearchTests.cs b/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerSearchTests.cs new file mode 100644 index 00000000..f4916885 --- /dev/null +++ b/Tests/TutorLizard.Web.Tests/Controllers/Browse/BrowseControllerSearchTests.cs @@ -0,0 +1,26 @@ +using AutoFixture; +using Microsoft.AspNetCore.Mvc; +using TutorLizard.Web.Models; +using TutorLizard.Web.Extensions; + +namespace TutorLizard.Web.Tests.Controllers.Browse; +public class BrowseControllerSearchTests : BrowseControllerTestsBase +{ + [Fact] + public void Search_WhenCalled_ShouldCorrectlyRedirectToAdsAction() + { + // Arrange + AdSearchCriteriaViewModel searchCriteria = Fixture.Create(); + string expectedSearchString = searchCriteria.ToDto().ToBase64String(); + + // Act + var result = BrowseController.Search(searchCriteria); + + // Assert + Assert.IsType(result); + Assert.Equal("Ads", ((RedirectToActionResult)result).ActionName); + Assert.Equal("Browse", ((RedirectToActionResult)result).ControllerName); + Assert.Equivalent(expectedSearchString, ((RedirectToActionResult)result).RouteValues?["search"]); + Assert.False(((RedirectToActionResult)result).RouteValues?.ContainsKey("id")); + } +} diff --git a/TutorLizard.Web/Controllers/BrowseController.cs b/TutorLizard.Web/Controllers/BrowseController.cs index 1bfc9bab..8739df48 100644 --- a/TutorLizard.Web/Controllers/BrowseController.cs +++ b/TutorLizard.Web/Controllers/BrowseController.cs @@ -61,12 +61,9 @@ public async Task Ads(int id = 1, [FromQuery] string? search = "" [HttpPost] public IActionResult Search([FromForm] AdSearchCriteriaViewModel searchCriteria) { - int pageNumber = 1; - string search = searchCriteria.AnySearch ? - searchCriteria.ToDto().ToBase64String() : - ""; + string search = searchCriteria.ToDto().ToBase64String(); - return RedirectToAction(nameof(Ads), "Browse", new { id = pageNumber, search }); + return RedirectToAction(nameof(Ads), "Browse", new { search }); } [Authorize] From 37adc7e10484faed776dcd57c00517c024785116 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Tue, 18 Jun 2024 14:17:08 +0200 Subject: [PATCH 07/12] Remove HandleGetBrowseAdsPageRequest() in BrowseController --- TutorLizard.Web/Controllers/BrowseController.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/TutorLizard.Web/Controllers/BrowseController.cs b/TutorLizard.Web/Controllers/BrowseController.cs index 8739df48..96259568 100644 --- a/TutorLizard.Web/Controllers/BrowseController.cs +++ b/TutorLizard.Web/Controllers/BrowseController.cs @@ -109,21 +109,6 @@ public async Task Schedule() return View(response); } - private async Task HandleGetBrowseAdsPageRequest(GetBrowseAdsPageRequest request, int pageNumber) - { - GetBrowseAdsPageResponse response = await _browseService.GetBrowseAdsPage(request); - - if (response.Success == false) - { - _uiMessagesService.ShowFailureMessage("Wystąpił błąd. Nie udało się się załadować ogłoszeń."); - return RedirectToAction("Index", "Home"); - } - - await AddCategoriesToViewBag(); - - return View(nameof(Ads), response); - } - private async Task AddCategoriesToViewBag() { GetCategoriesRequest request = new(); From fcefafa558f85fde14a946d36ff3ca7eb1bd98ac Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Tue, 18 Jun 2024 14:19:56 +0200 Subject: [PATCH 08/12] Remove deleted partial view's name from PartialNames --- TutorLizard.Web/Strings/PartialNames.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/TutorLizard.Web/Strings/PartialNames.cs b/TutorLizard.Web/Strings/PartialNames.cs index 6242c47c..6a4bd18b 100644 --- a/TutorLizard.Web/Strings/PartialNames.cs +++ b/TutorLizard.Web/Strings/PartialNames.cs @@ -8,5 +8,4 @@ public static class PartialNames public const string PendingAdRequestsListItem = "_PendingAdRequestsListItem"; public const string TutorAllAdRequestsListItem = "_TutorAllAdRequestsListItem"; public const string AdSearchCriteria = "_AdSearchCriteria"; - public const string GoToAdsPage = "_GoToAdsPage"; } From 4c0e2750dc9189dbae0a323ada96a0a7ef46d236 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Wed, 19 Jun 2024 12:25:28 +0200 Subject: [PATCH 09/12] Add searching logic to GetBrowseAdsPage() --- .../BrowseServiceGetBrowseAdsPageTests.cs | 1 + .../Services/BrowseService.cs | 134 ++++++++++++++++-- 2 files changed, 121 insertions(+), 14 deletions(-) diff --git a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs index 9b1517fd..fad3d510 100644 --- a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs +++ b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs @@ -114,6 +114,7 @@ public async Task GetBrowseAdsPage_WhenRequestIsValid_ShouldReturnCorrectAds(int int expectedAdsSkipped = (pageNumber - 1) * pageSize; List expectedAds = ads + .OrderBy(ad => ad.DateCreated) .Skip(expectedAdsSkipped) .Take(pageSize) .ToList(); diff --git a/TutorLizard.BusinessLogic/Services/BrowseService.cs b/TutorLizard.BusinessLogic/Services/BrowseService.cs index 5c24d985..cbfa12fb 100644 --- a/TutorLizard.BusinessLogic/Services/BrowseService.cs +++ b/TutorLizard.BusinessLogic/Services/BrowseService.cs @@ -32,21 +32,11 @@ public async Task GetBrowseAdsPage(GetBrowseAdsPageReq }; } - int adCount = await _adRepository.GetAll() - .CountAsync(); + IQueryable adsQuery = ApplySearchCriteria(_adRepository.GetAll(), request.SearchCriteria); + int totalPages = await GetTotalPagesCount(adsQuery, request); + adsQuery = ApplyPagination(adsQuery, request, totalPages); - int totalPages = adCount / request.PageSize; - if (adCount == 0 || adCount % request.PageSize != 0) - { - totalPages++; - } - - request.PageNumber = Math.Min(request.PageNumber, totalPages); - - int resultsToSkip = Math.Max((request.PageNumber - 1) * request.PageSize, 0); - List ads = await _adRepository.GetAll() - .Skip(resultsToSkip) - .Take(request.PageSize) + List ads = await adsQuery .Select(ad => new AdListItemDto() { Id = ad.Id, @@ -147,4 +137,120 @@ public async Task GetUsersSchedule(GetUsersScheduleReq return response; } + private IQueryable ApplySearchCriteria(IQueryable ads, AdSearchCriteriaDto searchCriteria) + { + ads = ApplySearchByText(ads, searchCriteria.Text); + ads = ApplySearchByPriceMin(ads, searchCriteria.PriceMin); + ads = ApplySearchByPriceMax(ads, searchCriteria.PriceMax); + ads = ApplySearchByLocation(ads, searchCriteria.Location); + ads = ApplySearchByIsRemote(ads, searchCriteria.IsRemote); + ads = ApplySearchByCategoryId(ads, searchCriteria.CategoryId); + + return ads; + } + + private IQueryable ApplySearchByText(IQueryable ads, string? text) + { + if (text is null) + { + return ads; + } + + text = text.ToLower(); + + ads = ads.Where(ad => + ad.Title.ToLower().Contains(text) || + ad.Subject.ToLower().Contains(text) || + ad.Description.ToLower().Contains(text)); + + return ads; + } + + private IQueryable ApplySearchByPriceMin(IQueryable ads, decimal? priceMin) + { + if (priceMin is null) + { + return ads; + } + + ads = ads.Where(ad => ad.Price >= priceMin); + + return ads; + } + + private IQueryable ApplySearchByPriceMax(IQueryable ads, decimal? priceMax) + { + if (priceMax is null) + { + return ads; + } + + ads = ads.Where(ad => ad.Price <= priceMax); + + return ads; + } + + private IQueryable ApplySearchByLocation(IQueryable ads, string? location) + { + if (location is null) + { + return ads; + } + + location = location.ToLower(); + + ads = ads.Where(ad => ad.Location.ToLower().Contains(location)); + + return ads; + } + + private IQueryable ApplySearchByIsRemote(IQueryable ads, bool? isRemote) + { + if (isRemote is null) + { + return ads; + } + + ads = ads.Where(ad => ad.IsRemote == isRemote); + + return ads; + } + + private IQueryable ApplySearchByCategoryId(IQueryable ads, int? categoryId) + { + if (categoryId is null) + { + return ads; + } + + ads = ads.Where(ad => ad.CategoryId == categoryId); + + return ads; + } + + private async Task GetTotalPagesCount(IQueryable ads, GetBrowseAdsPageRequest request) + { + int adCount = await ads.CountAsync(); + + int totalPages = adCount / request.PageSize; + if (adCount == 0 || adCount % request.PageSize != 0) + { + totalPages++; + } + + return totalPages; + } + private IQueryable ApplyPagination(IQueryable ads, GetBrowseAdsPageRequest request, int totalPages) + { + request.PageNumber = Math.Min(request.PageNumber, totalPages); + + int resultsToSkip = Math.Max((request.PageNumber - 1) * request.PageSize, 0); + + ads = ads + .OrderBy(ad => ad.DateCreated) + .Skip(resultsToSkip) + .Take(request.PageSize); + + return ads; + } } From bf7490e0f69891d15c46765669428d82f53e93d3 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Wed, 19 Jun 2024 12:40:54 +0200 Subject: [PATCH 10/12] Add TotalAds property to GetBrowseAdsPageResponse --- .../Browse/BrowseServiceGetBrowseAdsPageTests.cs | 7 ++++--- .../DTOs/Responses/GetBrowseAdsPageResponse.cs | 1 + TutorLizard.BusinessLogic/Services/BrowseService.cs | 13 +++++++------ TutorLizard.Web/Views/Browse/Ads.cshtml | 6 ++++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs index fad3d510..1900d0cf 100644 --- a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs +++ b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs @@ -81,20 +81,22 @@ public async Task GetBrowseAdsPage_WhenRequestIsValid_ShouldReturnCorrectNumberO [InlineData(1, 1, 0, 1)] [InlineData(1, 10, 101, 11)] [InlineData(12, 10, 101, 11)] - public async Task GetBrowseAdsPage_WhenRequestIsValid_ShouldReturnCorrectTotalPages(int pageNumber, int pageSize, int adCount, int expectedTotalPages) + public async Task GetBrowseAdsPage_WhenRequestIsValid_ShouldReturnCorrectTotals(int pageNumber, int pageSize, int expectedTotalAds, int expectedTotalPages) { // Arrange - var ads = CreateTestAds(adCount); + var ads = CreateTestAds(expectedTotalAds); SetupMockGetAllAds(ads); GetBrowseAdsPageRequest request = new(pageNumber, pageSize); // Act var response = await BrowseService.GetBrowseAdsPage(request); + int actualTotalAds = response.TotalAds; int actualTotalPages = response.TotalPages; // Assert Assert.True(response.Success); + Assert.Equal(expectedTotalAds, actualTotalAds); Assert.Equal(expectedTotalPages, actualTotalPages); } @@ -142,6 +144,5 @@ public async Task GetBrowseAdsPage_WhenRequestIsValid_ShouldReturnCorrectAds(int Assert.Equal(expected.Location, actual.Location); Assert.Equal(expected.IsRemote, actual.IsRemote); } - } } diff --git a/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs b/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs index d0b41367..2df5cde9 100644 --- a/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs +++ b/TutorLizard.BusinessLogic/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs @@ -6,5 +6,6 @@ public class GetBrowseAdsPageResponse public int PageNumber { get; set; } public int PageSize { get; set; } public int TotalPages { get; set; } + public int TotalAds { get; set; } public AdSearchCriteriaDto SearchCriteria { get; set; } = new(); } diff --git a/TutorLizard.BusinessLogic/Services/BrowseService.cs b/TutorLizard.BusinessLogic/Services/BrowseService.cs index cbfa12fb..5a725bee 100644 --- a/TutorLizard.BusinessLogic/Services/BrowseService.cs +++ b/TutorLizard.BusinessLogic/Services/BrowseService.cs @@ -33,7 +33,7 @@ public async Task GetBrowseAdsPage(GetBrowseAdsPageReq } IQueryable adsQuery = ApplySearchCriteria(_adRepository.GetAll(), request.SearchCriteria); - int totalPages = await GetTotalPagesCount(adsQuery, request); + (int totalPages, int totalAds) = await GetTotalCounts(adsQuery, request); adsQuery = ApplyPagination(adsQuery, request, totalPages); List ads = await adsQuery @@ -60,6 +60,7 @@ public async Task GetBrowseAdsPage(GetBrowseAdsPageReq PageNumber = request.PageNumber, PageSize = request.PageSize, TotalPages = totalPages, + TotalAds = totalAds, SearchCriteria = request.SearchCriteria }; @@ -228,17 +229,17 @@ private IQueryable ApplySearchByCategoryId(IQueryable ads, int? category return ads; } - private async Task GetTotalPagesCount(IQueryable ads, GetBrowseAdsPageRequest request) + private async Task<(int totalPages, int totalAds)> GetTotalCounts(IQueryable ads, GetBrowseAdsPageRequest request) { - int adCount = await ads.CountAsync(); + int totalAds = await ads.CountAsync(); - int totalPages = adCount / request.PageSize; - if (adCount == 0 || adCount % request.PageSize != 0) + int totalPages = totalAds / request.PageSize; + if (totalAds == 0 || totalAds % request.PageSize != 0) { totalPages++; } - return totalPages; + return (totalPages, totalAds); } private IQueryable ApplyPagination(IQueryable ads, GetBrowseAdsPageRequest request, int totalPages) { diff --git a/TutorLizard.Web/Views/Browse/Ads.cshtml b/TutorLizard.Web/Views/Browse/Ads.cshtml index 833dc7d3..ed40ffed 100644 --- a/TutorLizard.Web/Views/Browse/Ads.cshtml +++ b/TutorLizard.Web/Views/Browse/Ads.cshtml @@ -12,6 +12,12 @@ + +@if (Model.SearchCriteria.AnySearch) +{ +
Liczba wyników wyszukiwania: @Model.TotalAds
+} + @foreach (var ad in Model.Ads) { From 23414ce4fcdd49888d90d5394f9751eb352d0908 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Wed, 19 Jun 2024 14:56:44 +0200 Subject: [PATCH 11/12] Add tests for search logic in GetBrowseAdsPage --- .../BrowseServiceGetBrowseAdsPageTests.cs | 444 ++++++++++++++++++ .../Services/BrowseService.cs | 4 +- 2 files changed, 446 insertions(+), 2 deletions(-) diff --git a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs index 1900d0cf..a69d261b 100644 --- a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs +++ b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs @@ -145,4 +145,448 @@ public async Task GetBrowseAdsPage_WhenRequestIsValid_ShouldReturnCorrectAds(int Assert.Equal(expected.IsRemote, actual.IsRemote); } } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchTextIsNotNullOrWhiteSpace_ShouldApplySearchByText() + { + // Arrange + string searchText = Guid.NewGuid().ToString(); + int adCount = 6; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + Text = searchText + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + + Ad adToFindByTitle = ads[0]; + adToFindByTitle.Title = $"Test {searchText} Test"; + + Ad adToFindBySubject = ads[1]; + adToFindBySubject.Subject = searchText.ToUpper(); + + Ad adToFindByDescription = ads[2]; + adToFindByDescription.Description = searchText; + + Ad adNotToFindByTitle = ads[3]; + adNotToFindByTitle.Title = searchText[..^1]; + + Ad adNotToFindBySubject = ads[4]; + adNotToFindBySubject.Subject = searchText[1..]; + + Ad adNotToFindByDescription = ads[5]; + adNotToFindByDescription.Description = searchText[1..^1]; + + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adToFindByTitle.Id); + Assert.Contains(response.Ads, ad => ad.Id == adToFindBySubject.Id); + Assert.Contains(response.Ads, ad => ad.Id == adToFindByDescription.Id); + + Assert.DoesNotContain(response.Ads, ad => ad.Id == adNotToFindByTitle.Id); + Assert.DoesNotContain(response.Ads, ad => ad.Id == adNotToFindBySubject.Id); + Assert.DoesNotContain(response.Ads, ad => ad.Id == adNotToFindByDescription.Id); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("\n")] + [InlineData("\t")] + public async Task GetBrowseAdsPage_WhenSearchTextIsNullOrWhiteSpace_ShouldNotApplySearchByText(string? searchText) + { + // Arrange + int adCount = 10; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + Text = searchText + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Equal(adCount, response.TotalAds); + foreach (Ad ad in ads) + { + Assert.Contains(response.Ads, adDto => adDto.Id == ad.Id); + } + } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchPriceMinIsNotNull_ShouldApplySearchByPriceMin() + { + // Arrange + decimal searchPriceMin = 100m; + int adCount = 3; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + PriceMin = searchPriceMin + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + + Ad adWithPriceEqualToPriceMin = ads[0]; + adWithPriceEqualToPriceMin.Price = searchPriceMin; + + Ad adWithPriceLargerThanPriceMin = ads[1]; + adWithPriceLargerThanPriceMin.Price = searchPriceMin + 1; + + Ad adWithPriceLowerThanPriceMin = ads[2]; + adWithPriceLowerThanPriceMin.Price = searchPriceMin - 1; + + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceEqualToPriceMin.Id); + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceLargerThanPriceMin.Id); + Assert.DoesNotContain(response.Ads, ad => ad.Id == adWithPriceLowerThanPriceMin.Id); + } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchPriceMinIsNull_ShouldNotApplySearchByPriceMin() + { + // Arrange + decimal? searchPriceMin = null; + int adCount = 3; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + PriceMin = searchPriceMin + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + + Ad adWithPriceDecimalMinValue = ads[0]; + adWithPriceDecimalMinValue.Price = decimal.MinValue; + + Ad adWithPriceDecimalMaxValue = ads[1]; + adWithPriceDecimalMaxValue.Price = decimal.MaxValue; + + Ad adWithPriceZero = ads[2]; + adWithPriceZero.Price = decimal.Zero; + + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceDecimalMinValue.Id); + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceDecimalMaxValue.Id); + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceZero.Id); + } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchPriceMaxIsNotNull_ShouldApplySearchByPriceMin() + { + // Arrange + decimal searchPriceMax = 100m; + int adCount = 3; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + PriceMax = searchPriceMax + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + + Ad adWithPriceEqualToPriceMax = ads[0]; + adWithPriceEqualToPriceMax.Price = searchPriceMax; + + Ad adWithPriceLargerThanPriceMax = ads[1]; + adWithPriceLargerThanPriceMax.Price = searchPriceMax + 1; + + Ad adWithPriceLowerThanPriceMax = ads[2]; + adWithPriceLowerThanPriceMax.Price = searchPriceMax - 1; + + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceEqualToPriceMax.Id); + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceLowerThanPriceMax.Id); + Assert.DoesNotContain(response.Ads, ad => ad.Id == adWithPriceLargerThanPriceMax.Id); + } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchPriceMaxIsNull_ShouldNotApplySearchByPriceMin() + { + // Arrange + decimal? searchPriceMax = null; + int adCount = 3; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + PriceMax = searchPriceMax + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + + Ad adWithPriceDecimalMinValue = ads[0]; + adWithPriceDecimalMinValue.Price = decimal.MinValue; + + Ad adWithPriceDecimalMaxValue = ads[1]; + adWithPriceDecimalMaxValue.Price = decimal.MaxValue; + + Ad adWithPriceZero = ads[2]; + adWithPriceZero.Price = decimal.Zero; + + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceDecimalMinValue.Id); + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceDecimalMaxValue.Id); + Assert.Contains(response.Ads, ad => ad.Id == adWithPriceZero.Id); + } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchLocationIsNotNullOrWhiteSpace_ShouldApplySearchByLocation() + { + // Arrange + string searchLocation = Guid.NewGuid().ToString(); + int adCount = 2; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + Location = searchLocation + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + + Ad adToFindByLocation = ads[0]; + adToFindByLocation.Location = $"Test {searchLocation} Test"; + + Ad adNotToFindByLocation = ads[1]; + adNotToFindByLocation.Location = searchLocation[..^1]; + + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adToFindByLocation.Id); + Assert.DoesNotContain(response.Ads, ad => ad.Id == adNotToFindByLocation.Id); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("\n")] + [InlineData("\t")] + public async Task GetBrowseAdsPage_WhenSearchLocationIsNullOrWhiteSpace_ShouldNotApplySearchByLocation(string? searchLocation) + { + // Arrange + int adCount = 10; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + Location = searchLocation + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Equal(adCount, response.TotalAds); + foreach (Ad ad in ads) + { + Assert.Contains(response.Ads, adDto => adDto.Id == ad.Id); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetBrowseAdsPage_WhenSearchIsRemoteIsNotNull_ShouldApplySearchByIsRemote(bool searchIsRemote) + { + // Arrange + int adCount = 2; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + IsRemote = searchIsRemote + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + + Ad adToFindByIsRemote = ads[0]; + adToFindByIsRemote.IsRemote = searchIsRemote; + + Ad adNotToFindByIsRemote = ads[1]; + adNotToFindByIsRemote.IsRemote = !searchIsRemote; + + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adToFindByIsRemote.Id); + Assert.DoesNotContain(response.Ads, ad => ad.Id == adNotToFindByIsRemote.Id); + } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchIsRemoteIsNull_ShouldNotApplySearchByIsRemote() + { + // Arrange + bool? searchIsRemote = null; + int adCount = 2; + int pageNumber = 1; + int pageSize = adCount; + + AdSearchCriteriaDto searchCriteria = new() + { + IsRemote = searchIsRemote + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + var ads = CreateTestAds(adCount); + + Ad adWithIsRemoteTrue = ads[0]; + adWithIsRemoteTrue.IsRemote = true; + + Ad adWithIsRemoteFalse = ads[1]; + adWithIsRemoteFalse.IsRemote = false; + + SetupMockGetAllAds(ads); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adWithIsRemoteTrue.Id); + Assert.Contains(response.Ads, ad => ad.Id == adWithIsRemoteFalse.Id); + } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchCategoryIdIsNotNull_ShouldApplySearchByCategoryId() + { + // Arrange + int adCount = 2; + int pageNumber = 1; + int pageSize = adCount; + + + var ads = CreateTestAds(adCount); + + Ad adToFindByCategoryId = ads[0]; + + Ad adNotToFindByCategoryId = ads[1]; + adNotToFindByCategoryId.Category = CreateTestCategory(); + + SetupMockGetAllAds(ads); + + int searchCategoryId = adToFindByCategoryId.CategoryId; + + AdSearchCriteriaDto searchCriteria = new() + { + CategoryId = searchCategoryId + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + Assert.Contains(response.Ads, ad => ad.Id == adToFindByCategoryId.Id); + Assert.DoesNotContain(response.Ads, ad => ad.Id == adNotToFindByCategoryId.Id); + } + + [Fact] + public async Task GetBrowseAdsPage_WhenSearchCategoryIdIsNull_ShouldNotApplySearchByCategoryId() + { + // Arrange + int adCount = 2; + int pageNumber = 1; + int pageSize = adCount; + + + var ads = CreateTestAds(adCount); + + ads[0].Category = CreateTestCategory(); + ads[1].Category = CreateTestCategory(); + + SetupMockGetAllAds(ads); + + int? searchCategoryId = null; + + AdSearchCriteriaDto searchCriteria = new() + { + CategoryId = searchCategoryId + }; + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); + + // Act + var response = await BrowseService.GetBrowseAdsPage(request); + + // Assert + foreach (Ad ad in ads) + { + Assert.Contains(response.Ads, adDto => adDto.Id == ad.Id); + } + } } diff --git a/TutorLizard.BusinessLogic/Services/BrowseService.cs b/TutorLizard.BusinessLogic/Services/BrowseService.cs index 5a725bee..6bdb7855 100644 --- a/TutorLizard.BusinessLogic/Services/BrowseService.cs +++ b/TutorLizard.BusinessLogic/Services/BrowseService.cs @@ -152,7 +152,7 @@ private IQueryable ApplySearchCriteria(IQueryable ads, AdSearchCriteriaD private IQueryable ApplySearchByText(IQueryable ads, string? text) { - if (text is null) + if (String.IsNullOrWhiteSpace(text)) { return ads; } @@ -193,7 +193,7 @@ private IQueryable ApplySearchByPriceMax(IQueryable ads, decimal? priceM private IQueryable ApplySearchByLocation(IQueryable ads, string? location) { - if (location is null) + if (String.IsNullOrWhiteSpace(location)) { return ads; } From 6f57639d6b0293acce0ce6a44e0cf1e5256c7080 Mon Sep 17 00:00:00 2001 From: Adam Monikowski Date: Wed, 26 Jun 2024 14:20:20 +0200 Subject: [PATCH 12/12] Fix broken namespaces in tests --- .../Models/Dtos/AdSearchCriteriaDtoTests.cs | 6 +++--- .../Extensions/AdSearchCriteriaDtoExtensionsTests.cs | 2 +- .../Models/AdSearchCriteriaViewModelTests.cs | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs b/Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs index 37d797d8..5b1cf931 100644 --- a/Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs +++ b/Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs @@ -1,4 +1,4 @@ -using TutorLizard.BusinessLogic.Models.DTOs; +using TutorLizard.Shared.Models.DTOs; namespace TutorLizard.BusinessLogic.Tests.Models.Dtos; public class AdSearchCriteriaDtoTests @@ -25,8 +25,8 @@ public class AdSearchCriteriaDtoTests AdSearchCriteriaDto actual = new() { Text = text, - PriceMin = (decimal?) priceMin, - PriceMax = (decimal?) priceMax, + PriceMin = (decimal?)priceMin, + PriceMax = (decimal?)priceMax, Location = location, IsRemote = isRemote, CategoryId = categoryId, diff --git a/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs b/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs index be9700cf..ce4b0e4d 100644 --- a/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs +++ b/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs @@ -1,5 +1,5 @@ using AutoFixture; -using TutorLizard.BusinessLogic.Models.DTOs; +using TutorLizard.Shared.Models.DTOs; using TutorLizard.Web.Extensions; using TutorLizard.Web.Models; diff --git a/Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs b/Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs index 33d10927..aa77340f 100644 --- a/Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs +++ b/Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs @@ -1,5 +1,4 @@ -using TutorLizard.BusinessLogic.Models.DTOs; -using TutorLizard.Web.Models; +using TutorLizard.Web.Models; namespace TutorLizard.Web.Tests.Models; public class AdSearchCriteriaViewModelTests