Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Dfe.Data.Common.Infrastructure.CognitiveSearch.SearchByKeyword;
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Options;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

using AzureModels = Azure.Search.Documents.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure;

/// <summary>
Expand All @@ -17,7 +18,7 @@ public sealed class CognitiveSearchServiceAdapter<TSearchResult> : ISearchServic
{
private readonly ISearchByKeywordService _searchByKeywordService;
private readonly ISearchOptionsFactory _searchOptionsFactory;
private readonly IMapper<Response<SearchResults<TSearchResult>>, EstablishmentResults> _searchResponseMapper;
private readonly IMapper<Response<AzureModels.SearchResults<TSearchResult>>, SearchResults> _searchResponseMapper;

/// <summary>
/// The following dependencies include the core cognitive search service definition,
Expand All @@ -35,7 +36,7 @@ public sealed class CognitiveSearchServiceAdapter<TSearchResult> : ISearchServic
public CognitiveSearchServiceAdapter(
ISearchByKeywordService searchByKeywordService,
ISearchOptionsFactory searchOptionsFactory,
IMapper<Response<SearchResults<TSearchResult>>, EstablishmentResults> searchResponseMapper)
IMapper<Response<AzureModels.SearchResults<TSearchResult>>, SearchResults> searchResponseMapper)
{
_searchOptionsFactory = searchOptionsFactory;
_searchByKeywordService = searchByKeywordService;
Expand All @@ -62,14 +63,14 @@ public CognitiveSearchServiceAdapter(
/// Exception thrown if the data cannot be mapped
/// </exception>

public async Task<EstablishmentResults> SearchAsync(SearchContext searchContext)
public async Task<SearchResults> SearchAsync(SearchContext searchContext)
{
SearchOptions searchOptions =
_searchOptionsFactory.GetSearchOptions(searchContext.TargetCollection) ??
throw new ApplicationException(
$"Search options cannot be derived for {searchContext.TargetCollection}.");

Response<SearchResults<TSearchResult>> searchResults =
Response<AzureModels.SearchResults<TSearchResult>> searchResults =
await _searchByKeywordService.SearchAsync<TSearchResult>(
searchContext.SearchKeyword,
searchContext.TargetCollection,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mappers;

using AzureFacetResult = Azure.Search.Documents.Models.FacetResult;

/// <summary>
/// Maps from an Azure facet result to a collection of
/// T:Dfe.Data.SearchPrototype.SearchForEstablishments.Models.EstablishmentFacet
/// </summary>
public class AzureFacetResultToEstablishmentFacetsMapper : IMapper<Dictionary<string, IList<AzureFacetResult>>, EstablishmentFacets>
{
/// <summary>
/// Map from an Azure facet result to a collection of
/// T:Dfe.Data.SearchPrototype.SearchForEstablishments.Models.EstablishmentFacet
/// </summary>
/// <param name="facetResult">The Azure facet result</param>
/// <returns></returns>
public EstablishmentFacets MapFrom(Dictionary<string, IList<AzureFacetResult>> facetResult)
{
if (facetResult == null)
{
return new EstablishmentFacets();
}

var establishmentFacets = new List<EstablishmentFacet>();

foreach (var facetCategory in facetResult.Where(facet => facet.Value != null))
{
var values = facetCategory.Value.Select(f => new FacetResult((string)f.Value, f.Count)).ToList();
var establishmentFacet = new EstablishmentFacet(facetCategory.Key, values);

establishmentFacets.Add(establishmentFacet);
}

return new EstablishmentFacets (establishmentFacets);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public EstablishmentResults MapFrom(Response<SearchResults<Establishment>> input
ArgumentNullException.ThrowIfNull(input);

var results = input.Value.GetResults();

if (results.Any())
{
var mappedResults = results.Select(result =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public AzureSearchResultToEstablishmentMapper(
/// </exception>
public SearchForEstablishments.Models.Establishment MapFrom(Establishment input)
{
// TODO - only throw for really essential stuff
ArgumentException.ThrowIfNullOrEmpty(input.id, nameof(input.id));
ArgumentException.ThrowIfNullOrEmpty(input.ESTABLISHMENTNAME, nameof(input.ESTABLISHMENTNAME));
ArgumentException.ThrowIfNullOrEmpty(input.TYPEOFESTABLISHMENTNAME, nameof(input.TYPEOFESTABLISHMENTNAME));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ public sealed class CognitiveSearchServiceAdapterTests
private static CognitiveSearchServiceAdapter<Establishment> CreateServiceAdapterWith(
ISearchByKeywordService searchByKeywordService,
ISearchOptionsFactory searchOptionsFactory,
IMapper<Response<SearchResults<Establishment>>, EstablishmentResults> searchResponseMapper
IMapper<Response<SearchResults<Establishment>>, SearchResults> searchResponseMapper
) =>
new(searchByKeywordService, searchOptionsFactory, searchResponseMapper);

[Fact]
public async Task Search_WithValidSearchContext_ReturnsConfiguredResults()
public async Task Search_WithValidSearchContext_CallsMapper()
{
// arrange
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockSearchOptionsFactory();
var mockMapper = AzureSearchResponseToSearchResultsMapperTestDouble.MockFor(new EstablishmentResults());
var mockMapper = AzureSearchResponseToSearchResultsMapperTestDouble.MockFor(new SearchResults());

ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
Expand All @@ -36,14 +36,13 @@ public async Task Search_WithValidSearchContext_ReturnsConfiguredResults()
mockMapper);

// act
EstablishmentResults? response =
SearchResults? response =
await cognitiveSearchServiceAdapter.SearchAsync(
new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection"));

// assert
response.Establishments.Should().NotBeNull();
Mock.Get(mockService).Verify(SearchServiceTestDouble.SearchRequest("SearchKeyword", "TargetCollection"),Times.Once());
Mock.Get(mockSearchOptionsFactory).Verify(SearchOptionsFactoryTestDouble.SearchOption(), Times.Once());
Mock.Get(mockMapper).Verify(AzureSearchResponseToSearchResultsMapperTestDouble.MapFrom(), Times.Once());
Expand All @@ -54,7 +53,7 @@ public Task Search_WithNoSearchOptions_ThrowsApplicationException()
{
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockForNoOptions();
var mockMapper = AzureSearchResponseToSearchResultsMapperTestDouble.MockFor(new EstablishmentResults());
var mockMapper = AzureSearchResponseToSearchResultsMapperTestDouble.MockFor(new SearchResults());

// arrange
ISearchServiceAdapter cognitiveSearchServiceAdapter =
Expand All @@ -81,7 +80,7 @@ public async Task Search_WithNoResultsReturned_ReturnsEmptyResults()
// arrange
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockSearchOptionsFactory();
var mockMapper = AzureSearchResponseToSearchResultsMapperTestDouble.MockFor(new EstablishmentResults());
var mockMapper = AzureSearchResponseToSearchResultsMapperTestDouble.MockFor(new SearchResults());

ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
Expand All @@ -95,7 +94,9 @@ public async Task Search_WithNoResultsReturned_ReturnsEmptyResults()
targetCollection: "TargetCollection"));

// assert
response.Establishments.Should().BeEmpty();
response.Should().NotBeNull();
response.Establishments.Should().BeNull();
response.Facets.Should().BeNull();
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
using FluentAssertions;
using Xunit;

using AzureFacetResult = Azure.Search.Documents.Models.FacetResult;

namespace Dfe.Data.SearchPrototype.Infrastructure.Tests.Mappers;

public sealed class AzureFacetResultToEstablishmentFacetsMapperTests
{
IMapper<Dictionary<string, IList<AzureFacetResult>>, EstablishmentFacets> _facetResultToFacetMapper;

public AzureFacetResultToEstablishmentFacetsMapperTests()
{
_facetResultToFacetMapper =
new AzureFacetResultToEstablishmentFacetsMapper();
}

[Fact]
public void MapFrom_WithNoFacets_ReturnsNull()
{
// act
var mappedResult = _facetResultToFacetMapper.MapFrom(null!);

// assert
mappedResult.Facets.Should().BeEmpty();
}

[Fact]
public void MapFrom_WithStringFacetResults_ReturnsFacets()
{
// arrange
var azureFacetsResults = new FacetsResultsFakeBuilder()
.WithEducationPhaseFacet()
.WithAutoGeneratedFacet()
.Create();

// act
var mappedResult = _facetResultToFacetMapper.MapFrom(azureFacetsResults);

// assert
mappedResult.Should().NotBeNull();
mappedResult.Facets.Should().NotBeNullOrEmpty();
foreach (var azureFacet in azureFacetsResults)
{
mappedResult.Facets.First(facet => facet.Name == azureFacet.Key).Results.Should().NotBeNullOrEmpty();
}
}

[Fact]
public void MapFrom_WithNonStringFacetResults_ThrowsInvalidCastException()
{
// arrange
var azureFacetsResults = new FacetsResultsFakeBuilder()
.WithFacet(new List<object>() { true, "string2"})
.Create();

// act, assert
Action failedAction =
() => _facetResultToFacetMapper.MapFrom(azureFacetsResults);

InvalidCastException exception = Assert.Throws<InvalidCastException>(failedAction);

exception.Message.Should().Be("Unable to cast object of type 'System.Boolean' to type 'System.String'.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,24 @@ namespace Dfe.Data.SearchPrototype.Infrastructure.Tests.Mappers;

public sealed class AzureSearchResponseToEstablishmentResultMapperTests
{
IMapper<Establishment, SearchForEstablishments.Models.Establishment> _searchResultToEstablishmentMapper;
IMapper<Response<SearchResults<Establishment>>, EstablishmentResults> _searchResponseMapper;
IMapper<Establishment, Address> _searchResultToAddressMapper;

public AzureSearchResponseToEstablishmentResultMapperTests()
{
_searchResultToAddressMapper = new AzureSearchResultToAddressMapper();
_searchResultToEstablishmentMapper =
new AzureSearchResultToEstablishmentMapper(_searchResultToAddressMapper);
_searchResponseMapper =
new AzureSearchResponseToEstablishmentResultMapper(_searchResultToEstablishmentMapper);
new AzureSearchResponseToEstablishmentResultMapper(
new AzureSearchResultToEstablishmentMapper(
new AzureSearchResultToAddressMapper()));
}

[Fact]
public void MapFrom_WithValidSearchResults_ReturnsConfiguredEstablishments()
{
// arrange
List<SearchResult<Establishment>> searchResultDocuments =
SearchResultFake.SearchResults();
new SearchResultFakeBuilder().WithSearchResults().Create();
Response<SearchResults<Establishment>> searchResponseFake =
ResponseFake.WithSearchResults(searchResultDocuments);
new AzureSearchResponseTestDoubleBuilder().WithSearchResults(searchResultDocuments).Create();

// act
EstablishmentResults? mappedResult = _searchResponseMapper.MapFrom(searchResponseFake);
Expand All @@ -50,8 +47,12 @@ public void MapFrom_WithValidSearchResults_ReturnsConfiguredEstablishments()
public void MapFrom_WithEmptySearchResults_ReturnsEmptyList()
{
// arrange
var searchResultsDocuments = new SearchResultFakeBuilder()
.WithEmptySearchResult()
.Create();

Response<SearchResults<Establishment>> searchResponseFake =
ResponseFake.WithSearchResults(SearchResultFake.EmptySearchResult());
new AzureSearchResponseTestDoubleBuilder().WithSearchResults(searchResultsDocuments).Create();

// act
EstablishmentResults? result = _searchResponseMapper.MapFrom(searchResponseFake);
Expand Down Expand Up @@ -81,10 +82,13 @@ public void MapFrom_WithANullSearchResult_ThrowsInvalidOperationException()
{
// arrange
var searchResultDocuments =
SearchResultFake.SearchResults();
searchResultDocuments.Add(SearchResultFake.SearchResultWithDocument(null));
new SearchResultFakeBuilder()
.WithSearchResults()
.IncludeNullDocument()
.Create();

Response<SearchResults<Establishment>> searchResponseFake =
ResponseFake.WithSearchResults(searchResultDocuments);
new AzureSearchResponseTestDoubleBuilder().WithSearchResults(searchResultDocuments).Create();

// act.
_searchResponseMapper
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Azure.Search.Documents.Models;
using Azure;
using Moq;

namespace Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles;

public class AzureSearchResponseTestDoubleBuilder
{
private IEnumerable<SearchResult<Establishment>>? _searchResults;
private Dictionary<string, IList<FacetResult>>? _facetResults;

public AzureSearchResponseTestDoubleBuilder WithSearchResults(IEnumerable<SearchResult<Establishment>> searchResults)
{
_searchResults = searchResults;
return this;
}

public AzureSearchResponseTestDoubleBuilder WithFacets(Dictionary<string, IList<FacetResult>> facetResults)
{
_facetResults = facetResults;
return this;
}

public Response<SearchResults<Establishment>> Create()
{
var responseMock = new Mock<Response>();
return Response.FromValue(
SearchModelFactory.SearchResults(
_searchResults, 100, _facetResults, null, responseMock.Object), responseMock.Object);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ namespace Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles;

internal static class AzureSearchResponseToSearchResultsMapperTestDouble
{
public static IMapper<Response<SearchResults<Establishment>>, EstablishmentResults> DefaultMock() =>
Mock.Of<IMapper<Response<SearchResults<Establishment>>, EstablishmentResults>>();
public static Expression<Func<IMapper<Response<SearchResults<Establishment>>, EstablishmentResults>, EstablishmentResults>> MapFrom() =>
public static IMapper<Response<SearchResults<Establishment>>, SearchResults> DefaultMock() =>
Mock.Of<IMapper<Response<SearchResults<Establishment>>, SearchResults>>();
public static Expression<Func<IMapper<Response<SearchResults<Establishment>>, SearchResults>, SearchResults>> MapFrom() =>
mapper => mapper.MapFrom(It.IsAny<Response<SearchResults<Establishment>>>());

public static IMapper<Response<SearchResults<Establishment>>, EstablishmentResults> MockFor(EstablishmentResults establishments)
public static IMapper<Response<SearchResults<Establishment>>, SearchResults> MockFor(SearchResults searchResults)
{
var mapperMock = new Mock<IMapper<Response<SearchResults<Establishment>>, EstablishmentResults>>();
var mapperMock = new Mock<IMapper<Response<SearchResults<Establishment>>, SearchResults>>();

mapperMock.Setup(MapFrom()).Returns(establishments);
mapperMock.Setup(MapFrom()).Returns(searchResults);

return mapperMock.Object;
}

public static IMapper<Response<SearchResults<Establishment>>, EstablishmentResults> MockMapperThrowingArgumentException()
public static IMapper<Response<SearchResults<Establishment>>, SearchResults> MockMapperThrowingArgumentException()
{
var mapperMock = new Mock<IMapper<Response<SearchResults<Establishment>>, EstablishmentResults>>();
var mapperMock = new Mock<IMapper<Response<SearchResults<Establishment>>, SearchResults>>();

mapperMock.Setup(MapFrom()).Throws(new ArgumentException());

Expand Down
Loading