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..5b1cf931 --- /dev/null +++ b/Tests/TutorLizard.BusinessLogic.Tests/Models/Dtos/AdSearchCriteriaDtoTests.cs @@ -0,0 +1,59 @@ +using TutorLizard.Shared.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.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceGetBrowseAdsPageTests.cs index 8456f82a..a9774b45 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); } @@ -114,6 +116,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(); @@ -141,6 +144,449 @@ public async Task GetBrowseAdsPage_WhenRequestIsValid_ShouldReturnCorrectAds(int Assert.Equal(expected.Location, actual.Location); 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/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceTestsBase.cs b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceTestsBase.cs index 02dc7127..f4f1ae1d 100644 --- a/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceTestsBase.cs +++ b/Tests/TutorLizard.BusinessLogic.Tests/Services/Browse/BrowseServiceTestsBase.cs @@ -116,17 +116,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 5119292c..9ec3411f 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.Shared.Models.DTOs; using TutorLizard.Shared.Models.DTOs.Requests; using TutorLizard.Shared.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,16 @@ private GetBrowseAdsPageResponse CreateGetBrowseAdsPageResponse(bool success) .With(r => r.Success, success) .Create(); } + + private void SetupMockCategoryServiceGetAll(List categories) + { + MockCategoryService + .Setup(x => x.GetCategories(It.IsAny())) + .Returns(Task.FromResult( + new GetCategoriesResponse() + { + Success = true, + Categories = categories + })); + } } 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/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/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs b/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs new file mode 100644 index 00000000..ce4b0e4d --- /dev/null +++ b/Tests/TutorLizard.Web.Tests/Extensions/AdSearchCriteriaDtoExtensionsTests.cs @@ -0,0 +1,200 @@ +using AutoFixture; +using TutorLizard.Shared.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() + { + // 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); + } + + [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..aa77340f --- /dev/null +++ b/Tests/TutorLizard.Web.Tests/Models/AdSearchCriteriaViewModelTests.cs @@ -0,0 +1,59 @@ +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/Interfaces/Services/ICategoryService.cs b/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs new file mode 100644 index 00000000..547b1640 --- /dev/null +++ b/TutorLizard.BusinessLogic/Interfaces/Services/ICategoryService.cs @@ -0,0 +1,8 @@ +using TutorLizard.Shared.Models.DTOs.Requests; +using TutorLizard.Shared.Models.DTOs.Responses; + +namespace TutorLizard.BusinessLogic.Interfaces.Services; +public interface ICategoryService +{ + Task GetCategories(GetCategoriesRequest request); +} \ No newline at end of file diff --git a/TutorLizard.BusinessLogic/Services/BrowseService.cs b/TutorLizard.BusinessLogic/Services/BrowseService.cs index 949f2f7e..3c1f4a60 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, int totalAds) = await GetTotalCounts(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, @@ -69,7 +59,9 @@ public async Task GetBrowseAdsPage(GetBrowseAdsPageReq Ads = ads, PageNumber = request.PageNumber, PageSize = request.PageSize, - TotalPages = totalPages + TotalPages = totalPages, + TotalAds = totalAds, + SearchCriteria = request.SearchCriteria }; return response; @@ -146,4 +138,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 (String.IsNullOrWhiteSpace(text)) + { + 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 (String.IsNullOrWhiteSpace(location)) + { + 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<(int totalPages, int totalAds)> GetTotalCounts(IQueryable ads, GetBrowseAdsPageRequest request) + { + int totalAds = await ads.CountAsync(); + + int totalPages = totalAds / request.PageSize; + if (totalAds == 0 || totalAds % request.PageSize != 0) + { + totalPages++; + } + + return (totalPages, totalAds); + } + 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; + } } diff --git a/TutorLizard.BusinessLogic/Services/CategoryService.cs b/TutorLizard.BusinessLogic/Services/CategoryService.cs new file mode 100644 index 00000000..ae7a7ef2 --- /dev/null +++ b/TutorLizard.BusinessLogic/Services/CategoryService.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore; +using TutorLizard.BusinessLogic.Extensions; +using TutorLizard.BusinessLogic.Interfaces.Data.Repositories; +using TutorLizard.BusinessLogic.Interfaces.Services; +using TutorLizard.BusinessLogic.Models; +using TutorLizard.Shared.Models.DTOs; +using TutorLizard.Shared.Models.DTOs.Requests; +using TutorLizard.Shared.Models.DTOs.Responses; + +namespace TutorLizard.BusinessLogic.Services; +public class CategoryService : ICategoryService +{ + private readonly IDbRepository _categoryRepository; + + public CategoryService(IDbRepository categoryRepository) + { + _categoryRepository = categoryRepository; + } + + public async Task GetCategories(GetCategoriesRequest request) + { + List categories = await _categoryRepository + .GetAll() + .Select(category => category.ToDto()) + .ToListAsync(); + + return new GetCategoriesResponse() + { + Success = true, + Categories = categories + }; + } +} diff --git a/TutorLizard.Shared/Models/DTOs/AdSearchCriteriaDto.cs b/TutorLizard.Shared/Models/DTOs/AdSearchCriteriaDto.cs new file mode 100644 index 00000000..fdece446 --- /dev/null +++ b/TutorLizard.Shared/Models/DTOs/AdSearchCriteriaDto.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace TutorLizard.Shared.Models.DTOs; +public class AdSearchCriteriaDto +{ + 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; } + + [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.Shared/Models/DTOs/CategoryDto.cs b/TutorLizard.Shared/Models/DTOs/CategoryDto.cs index 61b3f942..e6402083 100644 --- a/TutorLizard.Shared/Models/DTOs/CategoryDto.cs +++ b/TutorLizard.Shared/Models/DTOs/CategoryDto.cs @@ -1,5 +1,4 @@ - -namespace TutorLizard.Shared.Models.DTOs; +namespace TutorLizard.Shared.Models.DTOs; public class CategoryDto { diff --git a/TutorLizard.Shared/Models/DTOs/Requests/CreateScheduleItemRequest.cs b/TutorLizard.Shared/Models/DTOs/Requests/CreateScheduleItemRequest.cs index 7435c413..57dea9c4 100644 --- a/TutorLizard.Shared/Models/DTOs/Requests/CreateScheduleItemRequest.cs +++ b/TutorLizard.Shared/Models/DTOs/Requests/CreateScheduleItemRequest.cs @@ -1,34 +1,28 @@ -using System; -using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace TutorLizard.Shared.Models.DTOs.Requests +namespace TutorLizard.Shared.Models.DTOs.Requests; + +public class CreateScheduleItemRequest { - public class CreateScheduleItemRequest + public CreateScheduleItemRequest() { - public CreateScheduleItemRequest() - { - } + } - public CreateScheduleItemRequest(int adId, - int userId, - DateTime dateTime) - { - AdId = adId; - UserId = userId; - DateTime = dateTime; - } + public CreateScheduleItemRequest(int adId, + int userId, + DateTime dateTime) + { + AdId = adId; + UserId = userId; + DateTime = dateTime; + } - public int AdId { get; set; } - public int UserId { get; set; } + public int AdId { get; set; } + public int UserId { get; set; } - [DisplayName("Data")] - [Required(ErrorMessage = "To pole jest wymagane")] - public DateTime DateTime { get; set; } + [DisplayName("Data")] + [Required(ErrorMessage = "To pole jest wymagane")] + public DateTime DateTime { get; set; } - } } diff --git a/TutorLizard.Shared/Models/DTOs/Requests/CreateScheduleItemRequestRequest.cs b/TutorLizard.Shared/Models/DTOs/Requests/CreateScheduleItemRequestRequest.cs index ed7f7a05..b2d8fa40 100644 --- a/TutorLizard.Shared/Models/DTOs/Requests/CreateScheduleItemRequestRequest.cs +++ b/TutorLizard.Shared/Models/DTOs/Requests/CreateScheduleItemRequestRequest.cs @@ -1,15 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace TutorLizard.Shared.Models.DTOs.Requests; -namespace TutorLizard.Shared.Models.DTOs.Requests +public class CreateScheduleItemRequestRequest { - public class CreateScheduleItemRequestRequest - { - public int StudentId { get; set; } - public int ScheduleItemId { get; set; } - public bool IsRemote { get; set; } - } + public int StudentId { get; set; } + public int ScheduleItemId { get; set; } + public bool IsRemote { get; set; } } diff --git a/TutorLizard.Shared/Models/DTOs/Requests/GetAvailableScheduleForAdRequest.cs b/TutorLizard.Shared/Models/DTOs/Requests/GetAvailableScheduleForAdRequest.cs index 95c8c5eb..8a25375c 100644 --- a/TutorLizard.Shared/Models/DTOs/Requests/GetAvailableScheduleForAdRequest.cs +++ b/TutorLizard.Shared/Models/DTOs/Requests/GetAvailableScheduleForAdRequest.cs @@ -1,13 +1,12 @@ -namespace TutorLizard.Shared.Models.DTOs.Requests +namespace TutorLizard.Shared.Models.DTOs.Requests; + +public class GetAvailableScheduleForAdRequest { - public class GetAvailableScheduleForAdRequest + public int AdId { get; set; } + public int StudentId { get; set; } + public GetAvailableScheduleForAdRequest(int adId, int studentId) { - public int AdId { get; set; } - public int StudentId { get; set; } - public GetAvailableScheduleForAdRequest(int adId, int studentId) - { - AdId = adId; - StudentId = studentId; - } + AdId = adId; + StudentId = studentId; } } diff --git a/TutorLizard.Shared/Models/DTOs/Requests/GetBrowseAdsPageRequest.cs b/TutorLizard.Shared/Models/DTOs/Requests/GetBrowseAdsPageRequest.cs index ca7bf186..191cfb8e 100644 --- a/TutorLizard.Shared/Models/DTOs/Requests/GetBrowseAdsPageRequest.cs +++ b/TutorLizard.Shared/Models/DTOs/Requests/GetBrowseAdsPageRequest.cs @@ -1,6 +1,21 @@ namespace TutorLizard.Shared.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.Shared/Models/DTOs/Requests/GetCategoriesRequest.cs b/TutorLizard.Shared/Models/DTOs/Requests/GetCategoriesRequest.cs new file mode 100644 index 00000000..06e96386 --- /dev/null +++ b/TutorLizard.Shared/Models/DTOs/Requests/GetCategoriesRequest.cs @@ -0,0 +1,5 @@ +namespace TutorLizard.Shared.Models.DTOs.Requests; + +public class GetCategoriesRequest +{ +} \ No newline at end of file diff --git a/TutorLizard.Shared/Models/DTOs/Requests/GetStudentsAcceptedAdsRequest.cs b/TutorLizard.Shared/Models/DTOs/Requests/GetStudentsAcceptedAdsRequest.cs index 15a32ae0..a90860e3 100644 --- a/TutorLizard.Shared/Models/DTOs/Requests/GetStudentsAcceptedAdsRequest.cs +++ b/TutorLizard.Shared/Models/DTOs/Requests/GetStudentsAcceptedAdsRequest.cs @@ -1,17 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace TutorLizard.Shared.Models.DTOs.Requests; -namespace TutorLizard.Shared.Models.DTOs.Requests +public class GetStudentsAcceptedAdsRequest { - public class GetStudentsAcceptedAdsRequest + public int StudentId { get; set; } + public GetStudentsAcceptedAdsRequest(int? studentId) { - public int StudentId { get; set; } - public GetStudentsAcceptedAdsRequest(int? studentId) - { - StudentId = (int)studentId; - } + StudentId = (int)studentId; } } diff --git a/TutorLizard.Shared/Models/DTOs/Requests/GetStudentsAdRequestsRequest.cs b/TutorLizard.Shared/Models/DTOs/Requests/GetStudentsAdRequestsRequest.cs index dedfc404..27a1e8b7 100644 --- a/TutorLizard.Shared/Models/DTOs/Requests/GetStudentsAdRequestsRequest.cs +++ b/TutorLizard.Shared/Models/DTOs/Requests/GetStudentsAdRequestsRequest.cs @@ -1,13 +1,10 @@ -using System; +namespace TutorLizard.Shared.Models.DTOs.Requests; -namespace TutorLizard.Shared.Models.DTOs.Requests +public class GetStudentsAdRequestsRequest { - public class GetStudentsAdRequestsRequest - { - public int StudentId { get; set; } - public GetStudentsAdRequestsRequest(int? studentId) - { - StudentId = (int)studentId; - } - } + public int StudentId { get; set; } + public GetStudentsAdRequestsRequest(int? studentId) + { + StudentId = (int)studentId; + } } diff --git a/TutorLizard.Shared/Models/DTOs/Requests/GetTutorsAdsRequest.cs b/TutorLizard.Shared/Models/DTOs/Requests/GetTutorsAdsRequest.cs index 4dcd43fc..2651cc2f 100644 --- a/TutorLizard.Shared/Models/DTOs/Requests/GetTutorsAdsRequest.cs +++ b/TutorLizard.Shared/Models/DTOs/Requests/GetTutorsAdsRequest.cs @@ -1,19 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TutorLizard.Shared.Models.DTOs.Responses; +namespace TutorLizard.Shared.Models.DTOs.Requests; -namespace TutorLizard.Shared.Models.DTOs.Requests +public class GetTutorsAdsRequest { - public class GetTutorsAdsRequest - { - public int TutorId { get; set; } + public int TutorId { get; set; } - public GetTutorsAdsRequest(int tutorId) - { - TutorId = tutorId; - } + public GetTutorsAdsRequest(int tutorId) + { + TutorId = tutorId; } } diff --git a/TutorLizard.Shared/Models/DTOs/Responses/CreateScheduleItemRequestResponse.cs b/TutorLizard.Shared/Models/DTOs/Responses/CreateScheduleItemRequestResponse.cs index 1c740620..4e2b1d22 100644 --- a/TutorLizard.Shared/Models/DTOs/Responses/CreateScheduleItemRequestResponse.cs +++ b/TutorLizard.Shared/Models/DTOs/Responses/CreateScheduleItemRequestResponse.cs @@ -1,15 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace TutorLizard.Shared.Models.DTOs.Responses; -namespace TutorLizard.Shared.Models.DTOs.Responses +public class CreateScheduleItemRequestResponse { - public class CreateScheduleItemRequestResponse - { - public bool Success { get; set; } - public int CreatedScheduleItemRequestId { get; set; } - public bool IsRemote { get; set; } - } + public bool Success { get; set; } + public int CreatedScheduleItemRequestId { get; set; } + public bool IsRemote { get; set; } } diff --git a/TutorLizard.Shared/Models/DTOs/Responses/CreateScheduleItemResponse.cs b/TutorLizard.Shared/Models/DTOs/Responses/CreateScheduleItemResponse.cs index db3cb29f..c5042d04 100644 --- a/TutorLizard.Shared/Models/DTOs/Responses/CreateScheduleItemResponse.cs +++ b/TutorLizard.Shared/Models/DTOs/Responses/CreateScheduleItemResponse.cs @@ -1,14 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace TutorLizard.Shared.Models.DTOs.Responses; -namespace TutorLizard.Shared.Models.DTOs.Responses +public class CreateScheduleItemResponse { - public class CreateScheduleItemResponse - { - public bool Success { get; set; } - public int CreatedItemId { get; set; } - } + public bool Success { get; set; } + public int CreatedItemId { get; set; } } diff --git a/TutorLizard.Shared/Models/DTOs/Responses/GetAvailableScheduleForAdResponse.cs b/TutorLizard.Shared/Models/DTOs/Responses/GetAvailableScheduleForAdResponse.cs index 53dec57d..9ee13d6b 100644 --- a/TutorLizard.Shared/Models/DTOs/Responses/GetAvailableScheduleForAdResponse.cs +++ b/TutorLizard.Shared/Models/DTOs/Responses/GetAvailableScheduleForAdResponse.cs @@ -1,16 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace TutorLizard.Shared.Models.DTOs.Responses; -namespace TutorLizard.Shared.Models.DTOs.Responses +public class GetAvailableScheduleForAdResponse { - public class GetAvailableScheduleForAdResponse - { - public List Items { get; set; } = new(); - public bool IsAccepted { get; set; } = true; - public bool IsRemote { get; set; } - public int AdId { get; set; } - } + public List Items { get; set; } = new(); + public bool IsAccepted { get; set; } = true; + public bool IsRemote { get; set; } + public int AdId { get; set; } } diff --git a/TutorLizard.Shared/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs b/TutorLizard.Shared/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs index 9f971ea8..c3946521 100644 --- a/TutorLizard.Shared/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs +++ b/TutorLizard.Shared/Models/DTOs/Responses/GetBrowseAdsPageResponse.cs @@ -6,4 +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.Shared/Models/DTOs/Responses/GetCategoriesResponse.cs b/TutorLizard.Shared/Models/DTOs/Responses/GetCategoriesResponse.cs new file mode 100644 index 00000000..6762c73e --- /dev/null +++ b/TutorLizard.Shared/Models/DTOs/Responses/GetCategoriesResponse.cs @@ -0,0 +1,6 @@ +namespace TutorLizard.Shared.Models.DTOs.Responses; +public class GetCategoriesResponse +{ + public bool Success { get; set; } + public List Categories { get; set; } = new(); +} diff --git a/TutorLizard.Shared/Models/DTOs/Responses/GetStudentsAcceptedAdsResponse.cs b/TutorLizard.Shared/Models/DTOs/Responses/GetStudentsAcceptedAdsResponse.cs index a822dd8b..9680e3bd 100644 --- a/TutorLizard.Shared/Models/DTOs/Responses/GetStudentsAcceptedAdsResponse.cs +++ b/TutorLizard.Shared/Models/DTOs/Responses/GetStudentsAcceptedAdsResponse.cs @@ -1,13 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace TutorLizard.Shared.Models.DTOs.Responses; -namespace TutorLizard.Shared.Models.DTOs.Responses +public class GetStudentsAcceptedAdsResponse { - public class GetStudentsAcceptedAdsResponse - { - public List Ads { get; set; } = new(); - } + public List Ads { get; set; } = new(); } diff --git a/TutorLizard.Shared/Models/DTOs/Responses/GetStudentsAdRequestsResponse.cs b/TutorLizard.Shared/Models/DTOs/Responses/GetStudentsAdRequestsResponse.cs index bd45a9ae..38a29d16 100644 --- a/TutorLizard.Shared/Models/DTOs/Responses/GetStudentsAdRequestsResponse.cs +++ b/TutorLizard.Shared/Models/DTOs/Responses/GetStudentsAdRequestsResponse.cs @@ -1,10 +1,6 @@ -using System; +namespace TutorLizard.Shared.Models.DTOs.Responses; -namespace TutorLizard.Shared.Models.DTOs.Responses +public class GetStudentsAdRequestsResponse { - public class GetStudentsAdRequestsResponse - { - public List AdRequests { get; set; } = new(); - } - + public List AdRequests { get; set; } = new(); } diff --git a/TutorLizard.Shared/Models/DTOs/Responses/GetTutorsAdsResponse.cs b/TutorLizard.Shared/Models/DTOs/Responses/GetTutorsAdsResponse.cs index 7457d858..df88b6a4 100644 --- a/TutorLizard.Shared/Models/DTOs/Responses/GetTutorsAdsResponse.cs +++ b/TutorLizard.Shared/Models/DTOs/Responses/GetTutorsAdsResponse.cs @@ -1,13 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace TutorLizard.Shared.Models.DTOs.Responses; -namespace TutorLizard.Shared.Models.DTOs.Responses +public class GetTutorsAdsResponse { - public class GetTutorsAdsResponse - { - public List AdList { get; set; } = new(); - } + public List AdList { get; set; } = new(); } diff --git a/TutorLizard.Shared/Models/DTOs/StudentScheduleItemRequestDto.cs b/TutorLizard.Shared/Models/DTOs/StudentScheduleItemRequestDto.cs index 8105d0aa..2a354ed4 100644 --- a/TutorLizard.Shared/Models/DTOs/StudentScheduleItemRequestDto.cs +++ b/TutorLizard.Shared/Models/DTOs/StudentScheduleItemRequestDto.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TutorLizard.Shared.Models.DTOs +namespace TutorLizard.Shared.Models.DTOs { public class StudentScheduleItemRequestDto { public int Id { get; set; } - public bool IsAccepted { get; set; } + public bool IsAccepted { get; set; } public DateTime DateCreated { get; set; } } } diff --git a/TutorLizard.Web/Controllers/BrowseController.cs b/TutorLizard.Web/Controllers/BrowseController.cs index bb106a5a..85a77e59 100644 --- a/TutorLizard.Web/Controllers/BrowseController.cs +++ b/TutorLizard.Web/Controllers/BrowseController.cs @@ -1,9 +1,13 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; using TutorLizard.BusinessLogic.Interfaces.Services; +using TutorLizard.Shared.Models.DTOs; using TutorLizard.Shared.Models.DTOs.Requests; using TutorLizard.Shared.Models.DTOs.Responses; using TutorLizard.Web.Interfaces.Services; +using TutorLizard.Web.Models; +using TutorLizard.Web.Extensions; namespace TutorLizard.Web.Controllers; public class BrowseController : Controller @@ -11,15 +15,18 @@ 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; + _categoryService = categoryService; _pageSize = 10; } public IActionResult Index() @@ -27,12 +34,16 @@ 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); + + AdSearchCriteriaDto? searchCriteria = search?.ToAdSearchCriteriaDto(); + searchCriteria ??= new(); + + GetBrowseAdsPageRequest request = new(pageNumber, pageSize, searchCriteria); GetBrowseAdsPageResponse response = await _browseService.GetBrowseAdsPage(request); @@ -42,8 +53,19 @@ public async Task Ads(int id = 1) return RedirectToAction("Index", "Home"); } - return View(response); + await AddCategoriesToViewBag(); + + return View(nameof(Ads), response); } + + [HttpPost] + public IActionResult Search([FromForm] AdSearchCriteriaViewModel searchCriteria) + { + string search = searchCriteria.ToDto().ToBase64String(); + + return RedirectToAction(nameof(Ads), "Browse", new { search }); + } + [Authorize] public async Task AdDetails(int id) { @@ -86,4 +108,14 @@ public async Task Schedule() GetUsersScheduleResponse response = await _browseService.GetUsersSchedule(request); return View(response); } + + private async Task AddCategoriesToViewBag() + { + GetCategoriesRequest request = new(); + GetCategoriesResponse response = await _categoryService.GetCategories(request); + + ViewBag.Categories = new SelectList(items: response.Categories, + dataValueField: nameof(CategoryDto.Id), + dataTextField: nameof(CategoryDto.Name)); + } } diff --git a/TutorLizard.Web/Controllers/TutorController.cs b/TutorLizard.Web/Controllers/TutorController.cs index 640a5198..b6c6409d 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/Extensions/AdSearchCriteriaDtoExtensions.cs b/TutorLizard.Web/Extensions/AdSearchCriteriaDtoExtensions.cs new file mode 100644 index 00000000..43c9d0d0 --- /dev/null +++ b/TutorLizard.Web/Extensions/AdSearchCriteriaDtoExtensions.cs @@ -0,0 +1,69 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using TutorLizard.Shared.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/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/Program.cs b/TutorLizard.Web/Program.cs index cc66a7b9..9a5837e1 100644 --- a/TutorLizard.Web/Program.cs +++ b/TutorLizard.Web/Program.cs @@ -35,6 +35,7 @@ .ValidateDataAnnotations(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddAuthentication(options => diff --git a/TutorLizard.Web/Strings/PartialNames.cs b/TutorLizard.Web/Strings/PartialNames.cs index ceeaa55b..6a4bd18b 100644 --- a/TutorLizard.Web/Strings/PartialNames.cs +++ b/TutorLizard.Web/Strings/PartialNames.cs @@ -7,4 +7,5 @@ public static class PartialNames public const string AdRequest = "_AdRequests"; public const string PendingAdRequestsListItem = "_PendingAdRequestsListItem"; public const string TutorAllAdRequestsListItem = "_TutorAllAdRequestsListItem"; + public const string AdSearchCriteria = "_AdSearchCriteria"; } diff --git a/TutorLizard.Web/Views/Browse/Ads.cshtml b/TutorLizard.Web/Views/Browse/Ads.cshtml index 7b4663b1..ed40ffed 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"; @@ -6,6 +7,17 @@

Ogłoszenia

+
+

Wyszukiwanie ogłoszeń

+ + + + +@if (Model.SearchCriteria.AnySearch) +{ +
Liczba wyników wyszukiwania: @Model.TotalAds
+} + @foreach (var ad in Model.Ads) { @@ -14,9 +26,17 @@
+ @if(Model.PageNumber > 1) { - Poprzednia strona + @if (Model.SearchCriteria.AnySearch) + { + Poprzednia strona + } + else + { + Poprzednia strona + } }
@@ -27,7 +47,14 @@
@if(Model.TotalPages > Model.PageNumber) { - Następna strona + @if(Model.SearchCriteria.AnySearch) + { + Następna strona + } + else + { + 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..0bc5debd --- /dev/null +++ b/TutorLizard.Web/Views/Shared/_AdSearchCriteria.cshtml @@ -0,0 +1,60 @@ +@model AdSearchCriteriaViewModel + +
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + @if (ViewBag.Categories is not null) + { + + } + +
+ +
+ +
+
+ + +
+
+
\ No newline at end of file