Skip to content

Commit

Permalink
Cl/handle search infrastructure exceptions (#35)
Browse files Browse the repository at this point in the history
* rename EstablishmentStatusCode

* Models folder

* remove NoResults status code

* build warnings

* PR comments
  • Loading branch information
CathLass authored Aug 8, 2024
1 parent 246e019 commit 39895e4
Show file tree
Hide file tree
Showing 29 changed files with 231 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Options;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure;

Expand Down Expand Up @@ -57,6 +58,10 @@ public CognitiveSearchServiceAdapter(
/// is unrecoverable, or no azure search results are returned which should never be the
/// case given no matches should return an empty wrapper result object.
/// </exception>
/// <exception cref="ArgumentException">
/// Exception thrown if the data cannot be mapped
/// </exception>

public async Task<EstablishmentResults> SearchAsync(SearchContext searchContext)
{
SearchOptions searchOptions =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Azure;
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mappers;

Expand All @@ -11,7 +11,7 @@ namespace Dfe.Data.SearchPrototype.Infrastructure.Mappers;
/// </summary>
public sealed class AzureSearchResponseToEstablishmentResultMapper : IMapper<Response<SearchResults<Establishment>>, EstablishmentResults>
{
private readonly IMapper<Establishment, SearchForEstablishments.Establishment> _azureSearchResultToEstablishmentMapper;
private readonly IMapper<Establishment, SearchForEstablishments.Models.Establishment> _azureSearchResultToEstablishmentMapper;

/// <summary>
/// The following mapping dependency provides the functionality to map from a raw Azure
Expand All @@ -21,7 +21,7 @@ public sealed class AzureSearchResponseToEstablishmentResultMapper : IMapper<Res
/// <param name="azureSearchResultToEstablishmentMapper">
/// Mapper used to map from the raw Azure search result to a T:Dfe.Data.SearchPrototype.Search.Establishment instance.
/// </param>
public AzureSearchResponseToEstablishmentResultMapper(IMapper<Establishment, SearchForEstablishments.Establishment> azureSearchResultToEstablishmentMapper)
public AzureSearchResponseToEstablishmentResultMapper(IMapper<Establishment, SearchForEstablishments.Models.Establishment> azureSearchResultToEstablishmentMapper)
{
_azureSearchResultToEstablishmentMapper = azureSearchResultToEstablishmentMapper;
}
Expand All @@ -40,6 +40,9 @@ public AzureSearchResponseToEstablishmentResultMapper(IMapper<Establishment, Sea
/// <exception cref="InvalidOperationException">
/// Exception thrown if an invalid document is derived from the Azure search result.
/// </exception>
/// <exception cref="ArgumentException">
/// Exception thrown if the data cannot be mapped
/// </exception>
public EstablishmentResults MapFrom(Response<SearchResults<Establishment>> input)
{
ArgumentNullException.ThrowIfNull(input);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mappers;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mappers;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mappers;

/// <summary>
/// Facilitates mapping from the received T:Dfe.Data.SearchPrototype.Infrastructure.Establishment
/// into the required T:Dfe.Data.SearchPrototype.SearchForEstablishments.Establishment object.
/// </summary>
public sealed class AzureSearchResultToEstablishmentMapper : IMapper<Establishment, SearchForEstablishments.Establishment>
public sealed class AzureSearchResultToEstablishmentMapper : IMapper<Establishment, SearchForEstablishments.Models.Establishment>
{
private readonly IMapper<Establishment, Address> _addressMapper;
private readonly IMapper<Establishment, EducationPhase> _educationPhaseMapper;
Expand Down Expand Up @@ -39,17 +39,17 @@ public AzureSearchResultToEstablishmentMapper(
/// <exception cref="ArgumentException">
/// Exception thrown if the id, name, or type of an establishment is not provided
/// </exception>
public SearchForEstablishments.Establishment MapFrom(Establishment input)
public SearchForEstablishments.Models.Establishment MapFrom(Establishment input)
{
ArgumentException.ThrowIfNullOrEmpty(input.id, nameof(input.id));
ArgumentException.ThrowIfNullOrEmpty(input.ESTABLISHMENTNAME, nameof(input.ESTABLISHMENTNAME));
ArgumentException.ThrowIfNullOrEmpty(input.TYPEOFESTABLISHMENTNAME, nameof(input.ESTABLISHMENTNAME));

var statusCode = input.ESTABLISHMENTSTATUSCODE == "1"
? StatusCode.Open
? EstablishmentStatusCode.Open
: input.ESTABLISHMENTSTATUSCODE == "0"
? StatusCode.Closed
: StatusCode.Unknown;
? EstablishmentStatusCode.Closed
: EstablishmentStatusCode.Unknown;

return new(
urn: input.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Dfe.Data.SearchPrototype.Infrastructure.Options;
using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
using FluentAssertions;
using Moq;
using Xunit;
Expand All @@ -21,12 +22,12 @@ IMapper<Response<SearchResults<Establishment>>, EstablishmentResults> searchResp
new(searchByKeywordService, searchOptionsFactory, searchResponseMapper);

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

ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
Expand All @@ -49,14 +50,18 @@ await cognitiveSearchServiceAdapter.SearchAsync(
}

[Fact]
public Task Search_With_Valid_SearchContext_No_Options_Returned_Throws_ApplicationException()
public Task Search_WithNoSearchOptions_ThrowsApplicationException()
{
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockForNoOptions();
var mockMapper = AzureSearchResponseToSearchResultsMapperTestDouble.MockFor(new EstablishmentResults());

// arrange
ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection"),
SearchOptionsFactoryTestDouble.MockForDefaultResult(),
AzureSearchResponseToSearchResultsMapperTestDouble.MockDefaultMapper());
mockService,
mockSearchOptionsFactory,
mockMapper);

// act.
return cognitiveSearchServiceAdapter
Expand All @@ -65,30 +70,54 @@ await serviceAdapter.SearchAsync(
new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection")))
.Should()
.ThrowAsync<ApplicationException>()
.WithMessage("Search options cannot be derived for TargetCollection.");
.Should()
.ThrowAsync<ApplicationException>()
.WithMessage("Search options cannot be derived for TargetCollection.");
}

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

ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
SearchServiceTestDouble.DefaultMock(),
SearchOptionsFactoryTestDouble.MockSearchOptionsFactory(),
AzureSearchResponseToSearchResultsMapperTestDouble.MockDefaultMapper());
mockService,
mockSearchOptionsFactory,
mockMapper);

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

// assert
response.Establishments.Should().BeEmpty();
}

[Fact]
public Task Search_MapperThrowsException_ExceptionPassesThrough()
{
// arrange
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockSearchOptionsFactory();
var mockMapper = AzureSearchResponseToSearchResultsMapperTestDouble.MockMapperThrowingArgumentException();

ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
mockService,
mockSearchOptionsFactory,
mockMapper);

// act, assert.
return cognitiveSearchServiceAdapter
.Invoking(async serviceAdapter =>
await serviceAdapter.SearchAsync(
new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection")))
.Should()
.ThrowAsync<ApplicationException>()
.WithMessage("Unable to derive search results based on input SearchKeyword.");
.Invoking(adapter => adapter.SearchAsync(new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection")))
.Should()
.ThrowAsync< ArgumentException>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
using Dfe.Data.SearchPrototype.Infrastructure.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles;
using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestHelpers;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
using FluentAssertions;
using Xunit;

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

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

public AzureSearchResponseToEstablishmentResultMapperTests()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
using FluentAssertions;
using Xunit;

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

public sealed class AzureSearchResultToEstablishmentMapperTests
{
IMapper<Establishment, SearchForEstablishments.Establishment> _establishmentMapper;
IMapper<Establishment, SearchForEstablishments.Address> _addressMapper;
IMapper<Establishment, SearchForEstablishments.EducationPhase> _educationPhaseMapper;
IMapper<Establishment, SearchForEstablishments.Models.Establishment> _establishmentMapper;
IMapper<Establishment, Address> _addressMapper;
IMapper<Establishment, EducationPhase> _educationPhaseMapper;

public AzureSearchResultToEstablishmentMapperTests()
{
Expand All @@ -21,16 +21,16 @@ public AzureSearchResultToEstablishmentMapperTests()
}

[Theory]
[InlineData("1", StatusCode.Open)]
[InlineData("0", StatusCode.Closed)]
[InlineData("", StatusCode.Unknown)]
public void MapFrom_With_Valid_Search_Result_Returns_Configured_Establishment(string statusCode, StatusCode expectedStatusCode)
[InlineData("1", EstablishmentStatusCode.Open)]
[InlineData("0", EstablishmentStatusCode.Closed)]
[InlineData("", EstablishmentStatusCode.Unknown)]
public void MapFrom_With_Valid_Search_Result_Returns_Configured_Establishment(string statusCode, EstablishmentStatusCode expectedStatusCode)
{
// arrange
Establishment establishmentFake = EstablishmentTestDouble.CreateWithStatusCode(statusCode);

// act
SearchForEstablishments.Establishment? result = _establishmentMapper.MapFrom(establishmentFake);
SearchForEstablishments.Models.Establishment? result = _establishmentMapper.MapFrom(establishmentFake);

// assert
result.Should().NotBeNull();
Expand Down Expand Up @@ -150,7 +150,7 @@ public void MapFrom_With_NullAddressValues_Returns_Configured_Establishment(
};

// act
SearchForEstablishments.Establishment? result = _establishmentMapper.MapFrom(establishmentFake);
SearchForEstablishments.Models.Establishment? result = _establishmentMapper.MapFrom(establishmentFake);

// assert
result.Should().NotBeNull();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Azure;
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
using Moq;
using System.Linq.Expressions;

Expand All @@ -23,12 +23,13 @@ public static IMapper<Response<SearchResults<Establishment>>, EstablishmentResul
return mapperMock.Object;
}

public static IMapper<Response<SearchResults<Establishment>>, EstablishmentResults> MockDefaultMapper()
public static IMapper<Response<SearchResults<Establishment>>, EstablishmentResults> MockMapperThrowingArgumentException()
{
var mockMapper = new Mock<IMapper<Response<SearchResults<Establishment>>, EstablishmentResults>>();
mockMapper.Setup(mapper => mapper.MapFrom(It.IsAny<Response<SearchResults<Establishment>>>()))
.Returns(new EstablishmentResults());
return mockMapper.Object;
var mapperMock = new Mock<IMapper<Response<SearchResults<Establishment>>, EstablishmentResults>>();

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

return mapperMock.Object;
}

internal static class EstablishmentFakes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Bogus;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using System.IO;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static ISearchOptionsFactory MockFor(SearchOptions searchOptions)

public static ISearchOptionsFactory MockSearchOptionsFactory() => MockFor(SearchOptionsFake);

public static ISearchOptionsFactory MockForDefaultResult() => MockFor(default!);
public static ISearchOptionsFactory MockForNoOptions() => MockFor(default!);
public static SearchOptions SearchOptionsFake => new()
{
SearchMode = SearchMode.Any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ public static ISearchByKeywordService MockForDefaultResult()
var validServiceResponseFake =
Task.FromResult<Response<SearchResults<Establishment>>>(default!);

return MockFor(validServiceResponseFake, string.Empty, string.Empty);
return MockFor(validServiceResponseFake, It.IsAny<string>(), It.IsAny<string>());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
using FluentAssertions;

namespace Dfe.Data.SearchPrototype.Infrastructure.Tests.TestHelpers;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

namespace Dfe.Data.SearchPrototype.SearchForEstablishments;

/// <summary>
/// Describes behaviour for an adaption of core search services infrastructure to allow
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Dfe.Data.SearchPrototype.SearchForEstablishments;
namespace Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

public class Address
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Dfe.Data.SearchPrototype.SearchForEstablishments;
namespace Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

/// <summary>
/// Object used to encapsulate the education phase of the retrieved establishment.
Expand Down
Loading

0 comments on commit 39895e4

Please sign in to comment.