From 7c451f4c1781203d083f06e678e90ed11c3a4440 Mon Sep 17 00:00:00 2001 From: Spencer O'HEGARTY Date: Mon, 30 Sep 2024 17:49:49 +0100 Subject: [PATCH 1/3] Completed build and tests --- .../Builders/ISearchOptionsBuilder.cs | 88 ++++++++++ .../Builders/SearchOptionsBuilder.cs | 154 ++++++++++++++++++ .../CognitiveSearchServiceAdapter.cs | 44 ++--- .../Infrastructure/CompositionRoot.cs | 2 + .../Builders/SearchOptionsBuilderTests.cs | 135 +++++++++++++++ ...itiveSearchServiceAdapterAndMapperTests.cs | 14 +- .../CognitiveSearchServiceAdapterTests.cs | 67 ++------ .../SearchOptionsBuilderTestDouble.cs | 34 ++++ 8 files changed, 450 insertions(+), 88 deletions(-) create mode 100644 Dfe.Data.SearchPrototype/Infrastructure/Builders/ISearchOptionsBuilder.cs create mode 100644 Dfe.Data.SearchPrototype/Infrastructure/Builders/SearchOptionsBuilder.cs create mode 100644 Dfe.Data.SearchPrototype/Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs create mode 100644 Dfe.Data.SearchPrototype/Infrastructure/Tests/TestDoubles/SearchOptionsBuilderTestDouble.cs diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Builders/ISearchOptionsBuilder.cs b/Dfe.Data.SearchPrototype/Infrastructure/Builders/ISearchOptionsBuilder.cs new file mode 100644 index 0000000..e409e45 --- /dev/null +++ b/Dfe.Data.SearchPrototype/Infrastructure/Builders/ISearchOptionsBuilder.cs @@ -0,0 +1,88 @@ +using Azure.Search.Documents; +using Azure.Search.Documents.Models; +using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.Usecase; + +namespace Dfe.Data.SearchPrototype.Infrastructure.Builders +{ + /// + /// Provides an abstraction on which to establish the behaviour + /// used to build configured instances. + /// + public interface ISearchOptionsBuilder + { + /// + /// Sets the number of search items to retrieve. + /// + /// + /// The number of results to return as specified. + /// + /// + /// The updated builder instance. + /// + ISearchOptionsBuilder WithSize(int? size); + + /// + /// Sets the mode of search to invoke, i.e. All or Any. + /// + /// + /// The mode of search to invoke, i.e. any search terms may match (Any), + /// or all search terms must match (All). + /// + /// + /// The updated builder instance. + /// + ISearchOptionsBuilder WithSearchMode(SearchMode searchMode); + + /// + /// Sets the option to include the total search results count in the search response. + /// + /// + /// The boolean value used to instruct the total count to be added to the response, or otherwise. + /// + /// + /// The updated builder instance. + /// + ISearchOptionsBuilder WithIncludeTotalCount(bool? includeTotalCount); + + /// + /// Sets the fields on which to establish the search. + /// + /// + /// List of fields over which to specify the search. + /// + /// + /// The updated builder instance. + /// + ISearchOptionsBuilder WithSearchFields(IList? searchFields); + + /// + /// Sets the facets to include in the search response. + /// + /// + /// List of facets to include in the search response. + /// + /// + /// The updated builder instance. + /// + ISearchOptionsBuilder WithFacets(IList? facets); + + /// + /// Sets the filters on which to establish the search. + /// + /// + /// List of filters on which to establish the search. + /// + /// + /// The updated builder instance. + /// + ISearchOptionsBuilder WithFilters(IList? filters); + + /// + /// Builds the configured instance of the type requested. + /// + /// + /// The fully configured (built) instance. + /// + SearchOptions Build(); + } +} diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Builders/SearchOptionsBuilder.cs b/Dfe.Data.SearchPrototype/Infrastructure/Builders/SearchOptionsBuilder.cs new file mode 100644 index 0000000..980d74b --- /dev/null +++ b/Dfe.Data.SearchPrototype/Infrastructure/Builders/SearchOptionsBuilder.cs @@ -0,0 +1,154 @@ +using Azure.Search.Documents; +using Azure.Search.Documents.Models; +using Dfe.Data.Common.Infrastructure.CognitiveSearch.Filtering; +using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.Usecase; + +namespace Dfe.Data.SearchPrototype.Infrastructure.Builders +{ + /// + /// Provides a concrete implementation of the abstraction, + /// which establishes a configured instance which conforms to the prescribed behaviour. + /// + public sealed class SearchOptionsBuilder : ISearchOptionsBuilder + { + private readonly SearchOptions _searchOptions; + private readonly ISearchFilterExpressionsBuilder _searchFilterExpressionsBuilder; + + private SearchMode? _searchMode; + private int? _size; + private bool? _includeTotalCount; + private IList? _searchFields; + private IList? _facets; + private IList? _filters; + + /// + /// The following dependency provides the + /// behaviour on which to generate fully configured, search filter string expressions based + /// on the provisioned request, the complete implementation of which is defined in the IOC container. + /// + /// + /// Builds the search filter expression required by Azure AI Search + /// + public SearchOptionsBuilder(ISearchFilterExpressionsBuilder searchFilterExpressionsBuilder) + { + _searchFilterExpressionsBuilder = searchFilterExpressionsBuilder; + _searchOptions = new SearchOptions(); + } + + /// + /// Sets the number of search items to retrieve. + /// + /// + /// The number of results to return as specified. + /// + /// + /// The updated builder instance. + /// + public ISearchOptionsBuilder WithSize(int? size) + { + _size = size; + return this; + } + + /// + /// Sets the mode of search to invoke, i.e. All or Any. + /// + /// + /// The mode of search to invoke, i.e. any search terms may match (Any), + /// or all search terms must match (All). + /// + /// + /// The updated builder instance. + /// + public ISearchOptionsBuilder WithSearchMode(SearchMode searchMode) + { + _searchMode = searchMode; + return this; + } + + /// + /// Sets the option to include the total search results count in the search response. + /// + /// + /// The boolean value used to instruct the total count to be added to the response, or otherwise. + /// + /// + /// The updated builder instance. + /// + public ISearchOptionsBuilder WithIncludeTotalCount(bool? includeTotalCount) + { + _includeTotalCount = includeTotalCount; + return this; + } + + /// + /// Sets the fields on which to establish the search. + /// + /// + /// List of fields over which to specify the search. + /// + /// + /// The updated builder instance. + /// + public ISearchOptionsBuilder WithSearchFields(IList? searchFields) + { + _searchFields = searchFields; + return this; + } + + /// + /// Sets the facets to include in the search response. + /// + /// + /// List of facets to include in the search response. + /// + /// + /// The updated builder instance. + /// + public ISearchOptionsBuilder WithFacets(IList? facets) + { + _facets = facets; + return this; + } + + /// + /// Sets the filters on which to establish the search. + /// + /// + /// List of filters on which to establish the search. + /// + /// + /// The updated builder instance. + /// + public ISearchOptionsBuilder WithFilters(IList? filters) + { + _filters = filters; + return this; + } + + /// + /// Builds the configured instance of the type requested. + /// + /// + /// The fully configured (built) instance. + /// + public SearchOptions Build() + { + _searchOptions.SearchMode = _searchMode; + _searchOptions.Size = _size; + _searchOptions.IncludeTotalCount = _includeTotalCount; + _searchFields?.ToList().ForEach(_searchOptions.SearchFields.Add); + _facets?.ToList().ForEach(_searchOptions.Facets.Add); + + if (_filters?.Count > 0) + { + _searchOptions.Filter = + _searchFilterExpressionsBuilder.BuildSearchFilterExpressions( + _filters.Select(filterRequest => + new SearchFilterRequest(filterRequest.FilterName, filterRequest.FilterValues))); + } + + return _searchOptions; + } + } +} diff --git a/Dfe.Data.SearchPrototype/Infrastructure/CognitiveSearchServiceAdapter.cs b/Dfe.Data.SearchPrototype/Infrastructure/CognitiveSearchServiceAdapter.cs index a6bccc0..249be71 100644 --- a/Dfe.Data.SearchPrototype/Infrastructure/CognitiveSearchServiceAdapter.cs +++ b/Dfe.Data.SearchPrototype/Infrastructure/CognitiveSearchServiceAdapter.cs @@ -1,9 +1,9 @@ using Azure; using Azure.Search.Documents; using Azure.Search.Documents.Models; -using Dfe.Data.Common.Infrastructure.CognitiveSearch.Filtering; using Dfe.Data.Common.Infrastructure.CognitiveSearch.SearchByKeyword; using Dfe.Data.SearchPrototype.Common.Mappers; +using Dfe.Data.SearchPrototype.Infrastructure.Builders; using Dfe.Data.SearchPrototype.Infrastructure.Options; using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.ServiceAdapters; using Dfe.Data.SearchPrototype.SearchForEstablishments.Models; @@ -19,10 +19,10 @@ namespace Dfe.Data.SearchPrototype.Infrastructure; public sealed class CognitiveSearchServiceAdapter : ISearchServiceAdapter where TSearchResult : class { private readonly ISearchByKeywordService _searchByKeywordService; - private readonly IMapper>, EstablishmentResults> _searchResultMapper; + private readonly IMapper>, EstablishmentResults> _searchResultMapper; private readonly IMapper>, EstablishmentFacets> _facetsMapper; private readonly AzureSearchOptions _azureSearchOptions; - private readonly ISearchFilterExpressionsBuilder _searchFilterExpressionsBuilder; + private readonly ISearchOptionsBuilder _searchOptionsBuilder; /// /// The following dependencies include the core cognitive search service definition, @@ -40,22 +40,22 @@ public sealed class CognitiveSearchServiceAdapter : ISearchServic /// /// Maps the raw Azure search response to the required /// - /// - /// Builds the search filter expression required by Azure AI Search + /// + /// Builds the search options by Azure AI Search /// public CognitiveSearchServiceAdapter( ISearchByKeywordService searchByKeywordService, IOptions azureSearchOptions, - IMapper>, EstablishmentResults> searchResultMapper, + IMapper>, EstablishmentResults> searchResultMapper, IMapper>, EstablishmentFacets> facetsMapper, - ISearchFilterExpressionsBuilder searchFilterExpressionsBuilder) + ISearchOptionsBuilder searchOptionsBuilder) { ArgumentNullException.ThrowIfNull(azureSearchOptions.Value); _azureSearchOptions = azureSearchOptions.Value; _searchByKeywordService = searchByKeywordService; _searchResultMapper = searchResultMapper; _facetsMapper = facetsMapper; - _searchFilterExpressionsBuilder = searchFilterExpressionsBuilder; + _searchOptionsBuilder = searchOptionsBuilder; } /// @@ -78,25 +78,15 @@ public CognitiveSearchServiceAdapter( /// public async Task SearchAsync(SearchServiceAdapterRequest searchServiceAdapterRequest) { - SearchOptions searchOptions = new() - { - SearchMode = (SearchMode)_azureSearchOptions.SearchMode, - Size = _azureSearchOptions.Size, - IncludeTotalCount = _azureSearchOptions.IncludeTotalCount, - }; - - searchServiceAdapterRequest.SearchFields?.ToList() - .ForEach(searchOptions.SearchFields.Add); - - searchServiceAdapterRequest.Facets?.ToList() - .ForEach(searchOptions.Facets.Add); - - if (searchServiceAdapterRequest.SearchFilterRequests?.Count > 0) - { - searchOptions.Filter = _searchFilterExpressionsBuilder.BuildSearchFilterExpressions( - searchServiceAdapterRequest.SearchFilterRequests - .Select(filterRequest => new SearchFilterRequest(filterRequest.FilterName, filterRequest.FilterValues))); - } + SearchOptions searchOptions = + _searchOptionsBuilder + .WithSearchMode((SearchMode)_azureSearchOptions.SearchMode) + .WithSize(_azureSearchOptions.Size) + .WithIncludeTotalCount(_azureSearchOptions.IncludeTotalCount) + .WithSearchFields(searchServiceAdapterRequest.SearchFields) + .WithFacets(searchServiceAdapterRequest.Facets) + .WithFilters(searchServiceAdapterRequest.SearchFilterRequests) + .Build(); Response> searchResults = await _searchByKeywordService.SearchAsync( diff --git a/Dfe.Data.SearchPrototype/Infrastructure/CompositionRoot.cs b/Dfe.Data.SearchPrototype/Infrastructure/CompositionRoot.cs index d61fc43..7d6205c 100644 --- a/Dfe.Data.SearchPrototype/Infrastructure/CompositionRoot.cs +++ b/Dfe.Data.SearchPrototype/Infrastructure/CompositionRoot.cs @@ -1,6 +1,7 @@ using Azure; using Azure.Search.Documents.Models; using Dfe.Data.SearchPrototype.Common.Mappers; +using Dfe.Data.SearchPrototype.Infrastructure.Builders; using Dfe.Data.SearchPrototype.Infrastructure.Mappers; using Dfe.Data.SearchPrototype.Infrastructure.Options; using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.ServiceAdapters; @@ -52,6 +53,7 @@ public static void AddCognitiveSearchAdaptorServices(this IServiceCollection ser .Bind(settings)); services.AddScoped(typeof(ISearchServiceAdapter), typeof(CognitiveSearchServiceAdapter)); + services.AddScoped(); services.AddSingleton(typeof(IMapper>, EstablishmentResults>), typeof(PageableSearchResultsToEstablishmentResultsMapper)); services.AddSingleton>, EstablishmentFacets>, AzureFacetResultToEstablishmentFacetsMapper>(); services.AddSingleton, AzureSearchResultToAddressMapper>(); diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs b/Dfe.Data.SearchPrototype/Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs new file mode 100644 index 0000000..596d71f --- /dev/null +++ b/Dfe.Data.SearchPrototype/Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs @@ -0,0 +1,135 @@ +using Azure.Search.Documents; +using Azure.Search.Documents.Models; +using Dfe.Data.Common.Infrastructure.CognitiveSearch.Filtering; +using Dfe.Data.SearchPrototype.Infrastructure.Builders; +using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles; +using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles.Shared; +using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.Usecase; +using FluentAssertions; +using Moq; +using Xunit; + +namespace Dfe.Data.SearchPrototype.Infrastructure.Tests.Builders +{ + public sealed class SearchOptionsBuilderTests + { + [Fact] + public void Build_WithSize_SearchOptionsWithCorrectSize() + { + // arrange + ISearchFilterExpressionsBuilder mockSearchFilterExpressionsBuilder = new FilterExpressionBuilderTestDouble().Create(); + + ISearchOptionsBuilder searchOptionsBuilder = new SearchOptionsBuilder(mockSearchFilterExpressionsBuilder); + + // act + SearchOptions searchOptions = searchOptionsBuilder.WithSize(size:100).Build(); + + // assert + searchOptions.Should().NotBeNull(); + searchOptions.Size.Should().Be(100); + } + + [Fact] + public void Build_WithSearchMode_SearchOptionsWithCorrectSearchMode() + { + // arrange + ISearchFilterExpressionsBuilder mockSearchFilterExpressionsBuilder = new FilterExpressionBuilderTestDouble().Create(); + + ISearchOptionsBuilder searchOptionsBuilder = new SearchOptionsBuilder(mockSearchFilterExpressionsBuilder); + + // act + SearchOptions searchOptions = searchOptionsBuilder.WithSearchMode(searchMode: SearchMode.Any).Build(); + + // assert + searchOptions.Should().NotBeNull(); + searchOptions.SearchMode.Should().Be(SearchMode.Any); + } + + [Fact] + public void Build_WithIncludeTotalCount_SearchOptionsWithIncludeTotalCount() + { + // arrange + ISearchFilterExpressionsBuilder mockSearchFilterExpressionsBuilder = new FilterExpressionBuilderTestDouble().Create(); + + ISearchOptionsBuilder searchOptionsBuilder = new SearchOptionsBuilder(mockSearchFilterExpressionsBuilder); + + // act + SearchOptions searchOptions = searchOptionsBuilder.WithIncludeTotalCount(includeTotalCount: true).Build(); + + // assert + searchOptions.Should().NotBeNull(); + searchOptions.IncludeTotalCount.Should().BeTrue(); + } + + [Fact] + public void Build_WithSearchFields_SearchOptionsWithWithSearchFields() + { + // arrange + ISearchFilterExpressionsBuilder mockSearchFilterExpressionsBuilder = new FilterExpressionBuilderTestDouble().Create(); + + ISearchOptionsBuilder searchOptionsBuilder = new SearchOptionsBuilder(mockSearchFilterExpressionsBuilder); + + // act + List searchFields = ["FIELD_1", "FIELD_2", "FIELD_3"]; + + SearchOptions searchOptions = searchOptionsBuilder.WithSearchFields(searchFields).Build(); + + // assert + searchOptions.Should().NotBeNull(); + searchOptions.SearchFields.Should().BeEquivalentTo(searchFields); + } + + [Fact] + public void Build_WithFacets_SearchOptionsWithWithFacets() + { + // arrange + ISearchFilterExpressionsBuilder mockSearchFilterExpressionsBuilder = new FilterExpressionBuilderTestDouble().Create(); + + ISearchOptionsBuilder searchOptionsBuilder = new SearchOptionsBuilder(mockSearchFilterExpressionsBuilder); + + // act + List searchFacets = ["FACET_1", "FACET_2", "FACET_3"]; + + SearchOptions searchOptions = searchOptionsBuilder.WithFacets(searchFacets).Build(); + + // assert + searchOptions.Should().NotBeNull(); + searchOptions.Facets.Should().BeEquivalentTo(searchFacets); + } + + [Fact] + public async Task Build_WithFilters_CallsFilterBuilder_WithComposedFilterRequests() + { + // arrange + var serviceAdapterInputFilterRequest = + new List() { FilterRequestFake.Create(), FilterRequestFake.Create() }; + + var mockSearchFilterExpressionsBuilder = new Mock(); + var requestMadeToFilterExpressionBuilder = new List(); + + mockSearchFilterExpressionsBuilder + .Setup(builder => builder.BuildSearchFilterExpressions(It.IsAny>())) + .Callback>((request) => + requestMadeToFilterExpressionBuilder = request.ToList()) + .Returns("some filter string"); + + ISearchOptionsBuilder searchOptionsBuilder = + new SearchOptionsBuilder(mockSearchFilterExpressionsBuilder.Object); + + // act + _ = searchOptionsBuilder.WithFilters(serviceAdapterInputFilterRequest).Build(); + + // assert + foreach (var filterRequest in serviceAdapterInputFilterRequest) + { + var matchingFilterRequest = + requestMadeToFilterExpressionBuilder + .First(request => + request.FilterKey == filterRequest.FilterName); + + matchingFilterRequest.Should().NotBeNull(); + matchingFilterRequest.FilterValues.Should().BeEquivalentTo(filterRequest.FilterValues); + } + } + } +} diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterAndMapperTests.cs b/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterAndMapperTests.cs index d94a819..672109f 100644 --- a/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterAndMapperTests.cs +++ b/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterAndMapperTests.cs @@ -1,11 +1,10 @@ using Azure; using Azure.Search.Documents.Models; -using Dfe.Data.Common.Infrastructure.CognitiveSearch.Filtering; using Dfe.Data.SearchPrototype.Common.Mappers; +using Dfe.Data.SearchPrototype.Infrastructure.Builders; using Dfe.Data.SearchPrototype.Infrastructure.Mappers; using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles; using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles.Shared; -using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword; using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.ServiceAdapters; using Dfe.Data.SearchPrototype.SearchForEstablishments.Models; using FluentAssertions; @@ -17,10 +16,7 @@ public sealed class CognitiveSearchServiceAdapterAndMapperTests { private readonly IMapper>, EstablishmentResults> _searchResponseMapper; private readonly IMapper>, EstablishmentFacets> _facetsMapper; - private readonly ISearchFilterExpressionsBuilder _mockSearchFilterExpressionsBuilder = - new FilterExpressionBuilderTestDouble() - .WithResponse("some_filter_name le some_value") - .Create(); + private readonly ISearchOptionsBuilder _searchOptionsBuilder = SearchOptionsBuilderTestDouble.MockFor(new Azure.Search.Documents.SearchOptions()); public CognitiveSearchServiceAdapterAndMapperTests() { @@ -53,7 +49,7 @@ public async Task Search_WithValidSearchContext_ReturnsResults() IOptionsTestDouble.IOptionsMockFor(options), _searchResponseMapper, _facetsMapper, - _mockSearchFilterExpressionsBuilder); + _searchOptionsBuilder); // act SearchResults? response = @@ -90,7 +86,7 @@ public async Task Search_WithNoFacetsReturned_ReturnsNullFacets() IOptionsTestDouble.IOptionsMockFor(options), _searchResponseMapper, _facetsMapper, - _mockSearchFilterExpressionsBuilder); + _searchOptionsBuilder); // act SearchResults? response = @@ -124,7 +120,7 @@ public async Task Search_WithNoResultsReturned_ReturnsEmptyResults() IOptionsTestDouble.IOptionsMockFor(options), _searchResponseMapper, _facetsMapper, - _mockSearchFilterExpressionsBuilder); + _searchOptionsBuilder); // act. var response = diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterTests.cs b/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterTests.cs index 96b3da8..cd6bba2 100644 --- a/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterTests.cs +++ b/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterTests.cs @@ -1,14 +1,13 @@ using Azure; using Azure.Search.Documents; using Azure.Search.Documents.Models; -using Dfe.Data.Common.Infrastructure.CognitiveSearch.Filtering; using Dfe.Data.Common.Infrastructure.CognitiveSearch.SearchByKeyword; using Dfe.Data.SearchPrototype.Common.Mappers; +using Dfe.Data.SearchPrototype.Infrastructure.Builders; using Dfe.Data.SearchPrototype.Infrastructure.Options; using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles; using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles.Shared; using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.ServiceAdapters; -using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.Usecase; using Dfe.Data.SearchPrototype.SearchForEstablishments.Models; using FluentAssertions; using Microsoft.Extensions.Options; @@ -19,23 +18,23 @@ namespace Dfe.Data.SearchPrototype.Infrastructure.Tests; public sealed class CognitiveSearchServiceAdapterTests { - private IMapper>, EstablishmentFacets> _mockFacetsMapper + private readonly IMapper>, EstablishmentFacets> _mockFacetsMapper = AzureFacetResultToEstablishmentFacetsMapperTestDouble.DefaultMock(); - private AzureSearchOptions _options - = AzureSearchOptionsTestDouble.Stub(); - private IMapper>, EstablishmentResults> _mockEstablishmentResultsMapper + private readonly AzureSearchOptions _options = AzureSearchOptionsTestDouble.Stub(); + private readonly IMapper>, EstablishmentResults> _mockEstablishmentResultsMapper = PageableSearchResultsToEstablishmentResultsMapperTestDouble.DefaultMock(); - private ISearchByKeywordService _mockSearchService; - private ISearchFilterExpressionsBuilder _mockFilterExpressionBuilder = new FilterExpressionBuilderTestDouble().Create(); + private readonly ISearchByKeywordService _mockSearchService; + private readonly ISearchOptionsBuilder _mockSearchOptionsBuilder = + new SearchOptionsBuilder(new FilterExpressionBuilderTestDouble().Create()); private static CognitiveSearchServiceAdapter CreateServiceAdapterWith( ISearchByKeywordService searchByKeywordService, IOptions searchOptions, IMapper>, EstablishmentResults> searchResponseMapper, IMapper>, EstablishmentFacets> facetsMapper, - ISearchFilterExpressionsBuilder filterExpressionsBuilder + ISearchOptionsBuilder searchOptionsBuilder ) => - new(searchByKeywordService, searchOptions, searchResponseMapper, facetsMapper, filterExpressionsBuilder); + new(searchByKeywordService, searchOptions, searchResponseMapper, facetsMapper, searchOptionsBuilder); public CognitiveSearchServiceAdapterTests() { @@ -55,8 +54,8 @@ public async Task Search_SendsCorrectRequestToSearchService() var responseMock = new Mock(); var searchServiceResponse = Response.FromValue( - SearchModelFactory.SearchResults( - new SearchResultFakeBuilder().WithSearchResults().Create(), 10, null, null, responseMock.Object), responseMock.Object); + SearchModelFactory.SearchResults( + new SearchResultFakeBuilder().WithSearchResults().Create(), 10, null, null, responseMock.Object), responseMock.Object); var mockService = new Mock(); mockService.Setup(service => service.SearchAsync(It.IsAny(), It.IsAny(), It.IsAny())) @@ -76,7 +75,7 @@ public async Task Search_SendsCorrectRequestToSearchService() IOptionsTestDouble.IOptionsMockFor(_options), _mockEstablishmentResultsMapper, _mockFacetsMapper, - _mockFilterExpressionBuilder); + _mockSearchOptionsBuilder); // act var response = await cognitiveSearchServiceAdapter.SearchAsync(searchServiceAdapterRequest); @@ -88,54 +87,18 @@ public async Task Search_SendsCorrectRequestToSearchService() searchOptionsPassedToSearchService?.Facets.Should().BeEquivalentTo(searchServiceAdapterRequest.Facets); } - [Fact] - public void Search_WithFilters_CallsFilterBuilder_WithComposedFilterRequests() - { - // arrange - var serviceAdapterInputFilterRequest = new List() { FilterRequestFake.Create(), FilterRequestFake.Create() }; - - var mockSearchFilterExpressionsBuilder = new Mock(); - var requestMadeToFilterExpressionBuilder = new List(); - mockSearchFilterExpressionsBuilder - .Setup(builder => builder.BuildSearchFilterExpressions(It.IsAny>())) - .Callback>((request) => { - requestMadeToFilterExpressionBuilder = request.ToList(); - }) - .Returns("some filter string"); - - var searchServiceAdapterRequest = SearchServiceAdapterRequestTestDouble.WithFilters(serviceAdapterInputFilterRequest); - - var adapter = new CognitiveSearchServiceAdapter( - _mockSearchService, - IOptionsTestDouble.IOptionsMockFor(_options), - _mockEstablishmentResultsMapper, - _mockFacetsMapper, - mockSearchFilterExpressionsBuilder.Object); - - // act - var response = adapter.SearchAsync(searchServiceAdapterRequest); - - // assert - foreach(var filterRequest in serviceAdapterInputFilterRequest) - { - var matchingFilterRequest = requestMadeToFilterExpressionBuilder.First(request => request.FilterKey == filterRequest.FilterName); - matchingFilterRequest.Should().NotBeNull(); - matchingFilterRequest.FilterValues.Should().BeEquivalentTo(filterRequest.FilterValues); - } - } - [Fact] public void Search_WithNoSearchOptions_ThrowsApplicationException() { // act, assert try { - var _ = new CognitiveSearchServiceAdapter( + _ = new CognitiveSearchServiceAdapter( _mockSearchService, IOptionsTestDouble.IOptionsMockFor(null!), _mockEstablishmentResultsMapper, _mockFacetsMapper, - _mockFilterExpressionBuilder); + _mockSearchOptionsBuilder); Assert.True(false); } catch (ArgumentNullException) @@ -157,7 +120,7 @@ public Task Search_MapperThrowsException_ExceptionPassesThrough() IOptionsTestDouble.IOptionsMockFor(_options), mockEstablishmentResultsMapper, _mockFacetsMapper, - _mockFilterExpressionBuilder); + _mockSearchOptionsBuilder); // act, assert. return cognitiveSearchServiceAdapter diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Tests/TestDoubles/SearchOptionsBuilderTestDouble.cs b/Dfe.Data.SearchPrototype/Infrastructure/Tests/TestDoubles/SearchOptionsBuilderTestDouble.cs new file mode 100644 index 0000000..999da1c --- /dev/null +++ b/Dfe.Data.SearchPrototype/Infrastructure/Tests/TestDoubles/SearchOptionsBuilderTestDouble.cs @@ -0,0 +1,34 @@ +using Azure.Search.Documents; +using Azure.Search.Documents.Models; +using Dfe.Data.SearchPrototype.Infrastructure.Builders; +using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.Usecase; +using Moq; + +namespace Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles +{ + internal static class SearchOptionsBuilderTestDouble + { + public static ISearchOptionsBuilder MockFor(SearchOptions searchOptions) + { + var mockSearchOptionsBuilder = new Mock(); + + mockSearchOptionsBuilder.Setup(searchOptionsBuilder => + searchOptionsBuilder.WithSize(It.IsAny())).Returns(mockSearchOptionsBuilder.Object); + mockSearchOptionsBuilder.Setup(searchOptionsBuilder => + searchOptionsBuilder.WithSearchMode(It.IsAny())).Returns(mockSearchOptionsBuilder.Object); + mockSearchOptionsBuilder.Setup(searchOptionsBuilder => + searchOptionsBuilder.WithIncludeTotalCount(It.IsAny())).Returns(mockSearchOptionsBuilder.Object); + mockSearchOptionsBuilder.Setup(searchOptionsBuilder => + searchOptionsBuilder.WithSearchFields(It.IsAny?>())).Returns(mockSearchOptionsBuilder.Object); + mockSearchOptionsBuilder.Setup(searchOptionsBuilder => + searchOptionsBuilder.WithFacets(It.IsAny>())).Returns(mockSearchOptionsBuilder.Object); + mockSearchOptionsBuilder.Setup(searchOptionsBuilder => + searchOptionsBuilder.WithFilters(It.IsAny>())).Returns(mockSearchOptionsBuilder.Object); + + mockSearchOptionsBuilder.Setup(searchOptionsBuilder => + searchOptionsBuilder.Build()).Returns(searchOptions); + + return mockSearchOptionsBuilder.Object; + } + } +} From 0b52290be04bc6689dbfdf8af0508b51f90e248e Mon Sep 17 00:00:00 2001 From: Spencer O'HEGARTY Date: Tue, 1 Oct 2024 09:21:53 +0100 Subject: [PATCH 2/3] Fixed unused async in test --- .../Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs b/Dfe.Data.SearchPrototype/Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs index 596d71f..141efa6 100644 --- a/Dfe.Data.SearchPrototype/Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs +++ b/Dfe.Data.SearchPrototype/Infrastructure/Tests/Builders/SearchOptionsBuilderTests.cs @@ -98,7 +98,7 @@ public void Build_WithFacets_SearchOptionsWithWithFacets() } [Fact] - public async Task Build_WithFilters_CallsFilterBuilder_WithComposedFilterRequests() + public void Build_WithFilters_CallsFilterBuilder_WithComposedFilterRequests() { // arrange var serviceAdapterInputFilterRequest = From 78a51470edd0b24c5aa99b0d0db3ce79e4be37d3 Mon Sep 17 00:00:00 2001 From: Spencer O'HEGARTY Date: Tue, 1 Oct 2024 16:30:56 +0100 Subject: [PATCH 3/3] Added additional test conditions in CognitiveSearchServiceAdapterTests vased on PR feedback --- .../Tests/CognitiveSearchServiceAdapterTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterTests.cs b/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterTests.cs index cd6bba2..b15d4ad 100644 --- a/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterTests.cs +++ b/Dfe.Data.SearchPrototype/Infrastructure/Tests/CognitiveSearchServiceAdapterTests.cs @@ -78,11 +78,14 @@ public async Task Search_SendsCorrectRequestToSearchService() _mockSearchOptionsBuilder); // act - var response = await cognitiveSearchServiceAdapter.SearchAsync(searchServiceAdapterRequest); + _ = await cognitiveSearchServiceAdapter.SearchAsync(searchServiceAdapterRequest); // assert keywordPassedToSearchService.Should().Be(searchServiceAdapterRequest.SearchKeyword); indexPassedToSearchService.Should().Be(_options.SearchIndex); + searchOptionsPassedToSearchService!.Size.Should().Be(_options.Size); + searchOptionsPassedToSearchService!.SearchMode.Should().Be((SearchMode)_options.SearchMode); + searchOptionsPassedToSearchService!.IncludeTotalCount.Should().Be(_options.IncludeTotalCount); searchOptionsPassedToSearchService!.SearchFields.Should().BeEquivalentTo(searchServiceAdapterRequest.SearchFields); searchOptionsPassedToSearchService?.Facets.Should().BeEquivalentTo(searchServiceAdapterRequest.Facets); }