public void GivenSearchParametersWhenSearchServicesGatewayMethodIsCalledThenItReturnsResultsMatchingNameAndDescription() { // arrange var services = EntityHelpers.CreateServices(10); var searchTerm = Randomm.Text(); searchTerm = " " + searchTerm;//15 Feb 2021 - Change made so we only search for whole words in the service description! - So we add a space. services.First().Name += searchTerm; services[1].Description += searchTerm; var expectedData = new List <Service>(); expectedData.Add(services.First()); expectedData.Add(services[1]); var requestParams = new SearchServicesRequest(); requestParams.Search = searchTerm; DatabaseContext.Services.AddRange(services); DatabaseContext.SaveChanges(); // act var gatewayResult = _classUnderTest.SearchServices(requestParams); var fullMatches = gatewayResult.FullMatchServices; // assert gatewayResult.Should().NotBeNull(); fullMatches.Should().NotBeNull(); fullMatches.Count.Should().Be(2); }
public void GivenSearchParametersWhenSearchServicesGatewayMethodIsCalledThenItReturnsMatchingRecordsWithOrgNameHigherThanServiceName() { // arrange var services = EntityHelpers.CreateServices(10); var searchTerm = Randomm.Text(); services.First().Name += searchTerm; services[1].Organization.Name += searchTerm; var expectedData = new List <Service>(); expectedData.Add(services.First()); expectedData.Add(services[1]); var requestParams = new SearchServicesRequest(); requestParams.Search = searchTerm; DatabaseContext.Services.AddRange(services); DatabaseContext.SaveChanges(); // act var gatewayResult = _classUnderTest.SearchServices(requestParams); var fullMatches = gatewayResult.FullMatchServices; // assert gatewayResult.Should().NotBeNull(); fullMatches.Should().NotBeNull(); fullMatches.Count.Should().Be(2); fullMatches[0].Name.Should().Be(services[1].Name); fullMatches[1].Name.Should().Be(services[0].Name); }
public void GivenAUrlEncodedSearchTermGatewayIsCalledWithDecodedTerm() // implementation of this test fixes a front-end bug, where front-end app accidentally encodes the url twice before calling an API - I don't we should be 'fixing' this on back-end API. { var expectedServices = Randomm.SSGatewayResult(); _mockServicesGateway.Setup(g => g.SearchServices(It.IsAny <SearchServicesRequest>())).Returns(expectedServices); // dummy setup - irrelevant for the test var searchTerm = Randomm.Text(); var urlencodedSearch = searchTerm.Replace(" ", "%2520"); var reqParams = new SearchServicesRequest(); reqParams.Search = urlencodedSearch; _classUnderTest.ExecuteGet(reqParams); _mockServicesGateway.Verify(g => g.SearchServices(It.Is <SearchServicesRequest>(p => p.Search == searchTerm)), Times.Once); }
public GetServiceResponseList ExecuteGet(SearchServicesRequest requestParams) { requestParams.Search = UrlHelper.DecodeParams(requestParams.Search); var postcodeIsGiven = !string.IsNullOrEmpty(requestParams.PostCode); var gatewayResponse = _servicesGateway.SearchServices(requestParams); var fullMServices = gatewayResponse.FullMatchServices.ToResponseServices(); var splitMServices = gatewayResponse.SplitMatchServices.ToResponseServices(); var metadata = new Metadata(); metadata.PostCode = postcodeIsGiven ? requestParams.PostCode : null; if (postcodeIsGiven) { try { //Get the postcode's coordinates Coordinate?postcodeCoord = _addressesGateway.GetPostcodeCoordinates(requestParams.PostCode); if (postcodeCoord.HasValue) { metadata.PostCodeLatitude = postcodeCoord.Value.Latitude; metadata.PostCodeLongitude = postcodeCoord.Value.Longitude; fullMServices.CalculateServiceLocationDistances(postcodeCoord.Value); splitMServices.CalculateServiceLocationDistances(postcodeCoord.Value); } else { metadata.Error = "Postcode coordinates not found."; } } catch (Exception ex) { metadata.Error = ex.Message; } } if (postcodeIsGiven) // And metadata error is empty { fullMServices.Sort(); // Sort by minimum service location's distance splitMServices.Sort(); // IComparator<Service> is defined on the object iteself } var usecaseResponse = ServiceFactory.SearchServiceUsecaseResponse(fullMServices, splitMServices, metadata); return(usecaseResponse); }
public IActionResult SearchServices([FromQuery] SearchServicesRequest requestParams) { try { var usecaseResult = _servicesUseCase.ExecuteGet(requestParams); return(Ok(usecaseResult)); } catch (Exception ex) when(ex.InnerException != null) { return(StatusCode(500, new ErrorResponse(ex.Message, ex.InnerException.Message))); } catch (Exception ex) { return(StatusCode(500, new ErrorResponse(ex.Message))); } }
public void GivenMultipleTaxonomyIdSearchParametersWhenSearchServicesGatewayMethodIsCalledThenItReturnsMatchingTaxonomyIdResults() { var taxonomy1 = EntityHelpers.CreateTaxonomy(); var taxonomy2 = EntityHelpers.CreateTaxonomy(); taxonomy1.Vocabulary = "demographic"; taxonomy2.Vocabulary = "category"; var services = EntityHelpers.CreateServices(); var serviceToFind1 = EntityHelpers.CreateService(); var serviceToFind2 = EntityHelpers.CreateService(); var serviceTaxonomy1 = EntityHelpers.CreateServiceTaxonomy(); var serviceTaxonomy2 = EntityHelpers.CreateServiceTaxonomy(); var serviceTaxonomy3 = EntityHelpers.CreateServiceTaxonomy(); var serviceTaxonomy4 = EntityHelpers.CreateServiceTaxonomy(); serviceTaxonomy1.Service = serviceToFind1; serviceTaxonomy1.Taxonomy = taxonomy1; serviceTaxonomy2.Service = serviceToFind1; serviceTaxonomy2.Taxonomy = taxonomy2; serviceTaxonomy3.Service = serviceToFind2; serviceTaxonomy3.Taxonomy = taxonomy1; serviceTaxonomy4.Service = serviceToFind2; serviceTaxonomy4.Taxonomy = taxonomy2; DatabaseContext.Services.AddRange(services); DatabaseContext.Services.Add(serviceToFind1); DatabaseContext.Services.Add(serviceToFind2); DatabaseContext.ServiceTaxonomies.Add(serviceTaxonomy1); DatabaseContext.ServiceTaxonomies.Add(serviceTaxonomy2); DatabaseContext.ServiceTaxonomies.Add(serviceTaxonomy3); DatabaseContext.ServiceTaxonomies.Add(serviceTaxonomy4); DatabaseContext.SaveChanges(); var requestParams = new SearchServicesRequest(); requestParams.TaxonomyIds = new List <int> { taxonomy1.Id, taxonomy2.Id }; var expectedData = new List <Service>(); expectedData.Add(serviceToFind1); expectedData.Add(serviceToFind2); var gatewayResult = _classUnderTest.SearchServices(requestParams); var fullMatches = gatewayResult.FullMatchServices; gatewayResult.Should().NotBeNull(); fullMatches.Should().NotBeNull(); fullMatches.Count.Should().Be(expectedData.Count); }
[TestCase(TestName = "Given user provided search term consisting of multiple words, When services get filtered in SearchService method, Then the returned services are categorized into Full user input match Or Split match.")] // done so to ensure the less relevant services are in the separate collection public void SearchServiceGatewaySeparatesOutFullMatchResultsFromSplitMatch() { // arrange var word1 = Randomm.Word(); var word2 = Randomm.Word(); var userSearchInput = $"{word1} {word2}"; var request = new SearchServicesRequest() { Search = userSearchInput }; var services = new List <Service>(); var serviceToFind1 = EntityHelpers.CreateService(); // full match serviceToFind1.Name += userSearchInput; var serviceToFind2 = EntityHelpers.CreateService(); // split match 1 serviceToFind2.Name += word1; var serviceToFind3 = EntityHelpers.CreateService(); // split match 2 serviceToFind3.Name += word2; services.Add(serviceToFind1); services.Add(serviceToFind2); services.Add(serviceToFind3); DatabaseContext.Services.AddRange(services); DatabaseContext.SaveChanges(); // act var gatewayResult = _classUnderTest.SearchServices(request); var fullMatches = gatewayResult.FullMatchServices; var splitMatches = gatewayResult.SplitMatchServices; // assert gatewayResult.Should().NotBeNull(); fullMatches.Should().NotBeNull(); splitMatches.Should().NotBeNull(); fullMatches.Should().Contain(s => s.Name.Contains(userSearchInput, StringComparison.OrdinalIgnoreCase)); fullMatches.Should().HaveCount(1); splitMatches.Should().Contain(s => s.Name.Contains(word1, StringComparison.OrdinalIgnoreCase)); splitMatches.Should().Contain(s => s.Name.Contains(word2, StringComparison.OrdinalIgnoreCase)); splitMatches.Should().NotContain(s => s.Name.Contains(userSearchInput, StringComparison.OrdinalIgnoreCase)); splitMatches.Should().HaveCount(2); }
public void WhenDoingPartialTextSearchMatchesWordsOf3orLessCharactersLongGetIgnored() //it's too short - these will be searched and found inside other words in DB { // arrange var shortWordList = new List <string> { "and", "a", "an", "the", "bfg", "42" }; var shortWord = shortWordList.RandomItem(); var word = Randomm.Word().Replace(shortWord, "test");// have to ensure the shortword is not contained in the actual word for the sake of test var userSearchInput = $"{shortWord} {word}"; var request = new SearchServicesRequest() { Search = userSearchInput }; var services = EntityHelpers.CreateServices(5).ToList(); //dummy services services.ForEach(s => s.Name = s.Name.Replace(word, "ssj")); //make sure they don't match the search word //assuming there's no full match. due to full match containing a shortword, the assertion at the bottom wouldn't be able to test what's needed. var serviceToFind = EntityHelpers.CreateService(); // word 1 match serviceToFind.Name = serviceToFind.Name.Replace(shortWord, "test"); //ensuring random hash does not contain shortword. for the assertion bellow to work as intended, the service name's hash part should not contain shortword. serviceToFind.Name += word; var serviceToNotFind = EntityHelpers.CreateService(); // shortword no match. this ensures that the test can fail if the implementation is wrong or not present. serviceToNotFind.Name = serviceToNotFind.Name.Replace(word, "1234"); // make sure the mismatching service does not contain a desired search term serviceToNotFind.Name += shortWord; services.Add(serviceToFind); services.Add(serviceToNotFind); DatabaseContext.Services.AddRange(services); DatabaseContext.SaveChanges(); // act var gatewayResult = _classUnderTest.SearchServices(request); var splitMatches = gatewayResult.SplitMatchServices; // assert gatewayResult.Should().NotBeNull(); splitMatches.Should().NotBeNull(); splitMatches.Should().NotContain(s => s.Name.Contains(shortWord, StringComparison.OrdinalIgnoreCase)); splitMatches.Should().HaveCount(1); }
public void UponFilteringServicesByAMultiWordInputTheReturnedResultsIncludePartialMatches() { // arrange var word1 = Randomm.Word(); var word2 = Randomm.Word(); var userSearchInput = $"{word1} {word2}"; var request = new SearchServicesRequest() { Search = userSearchInput }; var services = EntityHelpers.CreateServices(5).ToList(); // dummy services var serviceToFind1 = EntityHelpers.CreateService(); // full match serviceToFind1.Name += userSearchInput; var serviceToFind2 = EntityHelpers.CreateService(); // word 1 match serviceToFind2.Name += word1; var serviceToFind3 = EntityHelpers.CreateService(); // word 2 match serviceToFind3.Name += word2; services.Add(serviceToFind1); services.Add(serviceToFind2); services.Add(serviceToFind3); DatabaseContext.Services.AddRange(services); DatabaseContext.SaveChanges(); // act var gatewayResult = _classUnderTest.SearchServices(request); var fullMatches = gatewayResult.FullMatchServices; var splitMatches = gatewayResult.SplitMatchServices; var returnedServices = fullMatches.Concat(splitMatches).ToList(); // assert gatewayResult.Should().NotBeNull(); fullMatches.Should().NotBeNull(); splitMatches.Should().NotBeNull(); returnedServices.Should().Contain(s => s.Name.Contains(userSearchInput, StringComparison.OrdinalIgnoreCase)); returnedServices.Should().Contain(s => s.Name.Contains(word1, StringComparison.OrdinalIgnoreCase)); returnedServices.Should().Contain(s => s.Name.Contains(word2, StringComparison.OrdinalIgnoreCase)); returnedServices.Should().HaveCount(3); }
public void GivenSearchParametersWhenSearchServicesGatewayMethodIsCalledThenItReturnsMatchingSynonymGroupResults() { // arrange var synonymGroup1 = EntityHelpers.CreateSynonymGroupWithWords(5); var synonymGroup2 = EntityHelpers.CreateSynonymGroupWithWords(3); synonymGroup2.SynonymWords.ToList()[1].Word = synonymGroup1.SynonymWords.ToList()[1].Word; var services = EntityHelpers.CreateServices(); services.ForEach(s => s.Name = "irrelevant"); var serviceToFind1 = EntityHelpers.CreateService(); var serviceToFind2 = EntityHelpers.CreateService(); var searchTerm = synonymGroup1.SynonymWords.ToList()[1].Word; var requestParams = new SearchServicesRequest(); requestParams.Search = searchTerm; serviceToFind1.Name += synonymGroup1.SynonymWords.ToList()[4].Word; serviceToFind2.Name += synonymGroup2.SynonymWords.ToList()[2].Word; DatabaseContext.SynonymGroups.Add(synonymGroup1); DatabaseContext.SynonymGroups.Add(synonymGroup2); DatabaseContext.Services.AddRange(services); DatabaseContext.Services.Add(serviceToFind1); DatabaseContext.Services.Add(serviceToFind2); DatabaseContext.SaveChanges(); var expectedData = new List <Service>(); expectedData.Add(serviceToFind1); expectedData.Add(serviceToFind2); // act var gatewayResult = _classUnderTest.SearchServices(requestParams); var fullMatches = gatewayResult.FullMatchServices; var splitMatches = gatewayResult.SplitMatchServices; var returnedServices = fullMatches.Concat(splitMatches).ToList(); // assert gatewayResult.Should().NotBeNull(); fullMatches.Should().NotBeNull(); splitMatches.Should().NotBeNull(); returnedServices.Count.Should().Be(expectedData.Count); }
public void WholeAndSplitUserInputMatchesAreReturnedInCorrectRank() { // arrange var searchWord1 = Randomm.Word(); // word that won't match any results, however 1 of its synonyms from synonym group will var searchWord2 = Randomm.Word(); // same as above, but the matching synonym will be in another synonym group var irrelevantWord = Randomm.Word(); // a control word, that won't match anything, nor its synonyms will. var userInput = $"{searchWord1} " + $"{irrelevantWord} " + $"{searchWord2}"; var request = new SearchServicesRequest(); request.Search = userInput; var bridgeSyn1Word = Utility.SuperSetOfString(searchWord1); // A superset word of search word 1 that will relate to a synonym word inside synonym group 1 - this word has no match in the DB var bridgeSyn2Word = Utility.SuperSetOfString(searchWord2); // A superset word of search word 2 that will relate to a synonym word inside synonym group 2 - this word has no match in the DB var synWord1 = Randomm.Word(); // synonym within the same synonym group (1) as a word related to search word 1 - this word has a match in the DB var synWord2 = Randomm.Word(); // synonym within the same synonym group (1) as a word related to search word 1 - this word has a match in the DB var synWord3 = Randomm.Word(); // synonym within the same synonym group (2) as a word related to search word 2 - this word has a match in the DB var synonymGroup1 = EntityHelpers.CreateSynonymGroupWithWords(); // relevant group with dummy synonym words var synonymGroup2 = EntityHelpers.CreateSynonymGroupWithWords(); // relevant group with dummy synonym words var dummySynGroup = EntityHelpers.CreateSynonymGroupWithWords(); // dummy synonym group that should not be picked up var bridgeSynonym1 = EntityHelpers.SynWord(synonymGroup1, bridgeSyn1Word); // synonym that has no match in DB, however it bridges user input search word with the synonym group 1 var bridgeSynonym2 = EntityHelpers.SynWord(synonymGroup2, bridgeSyn2Word); var matchSynonym1 = EntityHelpers.SynWord(synonymGroup1, synWord1); // creating a synonym word object to insert that will have a match, creating a link with synonym group 1 var matchSynonym2 = EntityHelpers.SynWord(synonymGroup1, synWord2); var matchSynonym3 = EntityHelpers.SynWord(synonymGroup2, synWord3); synonymGroup1.SynonymWords.Add(bridgeSynonym1); // added bridge synonym to the synonym group synonymGroup2.SynonymWords.Add(bridgeSynonym2); synonymGroup1.SynonymWords.Add(matchSynonym1); // added match synonym into a synonym group synonymGroup1.SynonymWords.Add(matchSynonym2); synonymGroup2.SynonymWords.Add(matchSynonym3); var services = EntityHelpers.CreateServices(5); // creating list of dummy services that should not be found var matchService1 = EntityHelpers.CreateService(); // service that is intended to be found through the synonym of synonym group 1 var matchService2 = EntityHelpers.CreateService(); // service that is intended to be found through the synonym of synonym group 1 var matchService3 = EntityHelpers.CreateService(); // service that is intended to be found through the synonym of synonym group 2 var matchService4 = EntityHelpers.CreateService(); // service that is intended to be found through the main search term matchService1.Name += searchWord2; // creating a link between a service and a match synonym 1 // creating a link between a service and a match synonym 2 matchService2.Description += " " + synWord2; //15 Feb 2021 - Change made so we only search for whole words in the service description! - So we add a space. matchService3.Organization.Name += synWord3; // creating a link between a service and a match synonym 3 matchService4.Organization.Name += searchWord1; // creating a link between a service and a main search word services.AddMany(matchService1, matchService2, matchService3, matchService4); // include match services into a to be inserted services collection DatabaseContext.SynonymGroups.AddRange(synonymGroup1); // adding synonym groups containing synonym words into a database DatabaseContext.SynonymGroups.AddRange(synonymGroup2); DatabaseContext.SynonymGroups.AddRange(dummySynGroup); DatabaseContext.Services.AddRange(services); // adding services into a database DatabaseContext.SaveChanges(); // act var gatewayResult = _classUnderTest.SearchServices(request); var splitMatches = gatewayResult.SplitMatchServices; var fullMatches = gatewayResult.FullMatchServices; // assert splitMatches.Should().HaveCount(4); fullMatches.Should().HaveCount(0); splitMatches[0].Name.Should().Be(matchService4.Name); splitMatches[1].Name.Should().Be(matchService1.Name); splitMatches[2].Name.Should().Be(matchService3.Name); splitMatches[3].Name.Should().Be(matchService2.Name); }
public SearchServiceGatewayResult SearchServices(SearchServicesRequest requestParams) { var baseQuery = _context.Services .Include(s => s.Image) .Include(s => s.Organization) .Include(s => s.ServiceLocations) .Include(s => s.ServiceTaxonomies) .ThenInclude(st => st.Taxonomy) .Where(s => s.Organization.Status.ToLower() == "published") .AsEnumerable(); IEnumerable <Service> fullMatchServicesQuery = baseQuery, splitMatchServicesQuery = baseQuery; List <ServiceEntity> fullMatchServices = new List <ServiceEntity>(), splitMatchServices = new List <ServiceEntity>(); var synonyms = new HashSet <string>(); var demographicTaxonomies = _context.Taxonomies .Where(t => t.Vocabulary == "demographic" && requestParams.TaxonomyIds.Any(ti => ti == t.Id)) .Select(t => t.Id).ToList(); var categoryTaxonomies = _context.Taxonomies .Where(t => t.Vocabulary == "category" && requestParams.TaxonomyIds.Any(ti => ti == t.Id)) .Select(t => t.Id).ToList(); if (demographicTaxonomies != null && demographicTaxonomies.Count != 0) { fullMatchServicesQuery = fullMatchServicesQuery .Where(s => s.ServiceTaxonomies .Any(st => demographicTaxonomies.Contains(st.TaxonomyId))); } if (categoryTaxonomies != null && categoryTaxonomies.Count != 0) { fullMatchServicesQuery = fullMatchServicesQuery .Where(s => s.ServiceTaxonomies .Any(st => categoryTaxonomies.Contains(st.TaxonomyId))); } if (!string.IsNullOrWhiteSpace(requestParams.Search)) { var searchInputText = requestParams.Search.ToLower(); var splitWords = searchInputText .Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); var moreThan1SearchInputWord = splitWords.Count > 1; // fuzzy search won't execute if only 1 word entered splitWords = splitWords.Where(k => k.Length > 3).ToList(); // filter short words var matchedSynonyms = _context.SynonymWords // This variable is not being used! Does it need to be here? .Include(sw => sw.Group) .ThenInclude(sg => sg.SynonymWords) .Where(x => x.Word.ToLower().Contains(searchInputText)) .Select(sw => sw.Group.SynonymWords.Select(sw => synonyms.Add(sw.Word.ToLower())) // hard to track side effects >_< .ToList()) .ToList(); // Filter on service organisation name Predicate <Service> containsUserInput = service => service.Organization.Name.ToLower().Contains(searchInputText); Predicate <Service> containsAnySynonym = service => synonyms.Any(sn => service.Organization.Name.ToLower().Contains(sn)); ApplyFullTextFilter(containsUserInput, containsAnySynonym, synonyms, fullMatchServicesQuery, fullMatchServices); // Filter on service name containsUserInput = service => service.Name.ToLower().Contains(searchInputText); containsAnySynonym = service => synonyms.Any(sn => service.Name.ToLower().Contains(sn)); ApplyFullTextFilter(containsUserInput, containsAnySynonym, synonyms, fullMatchServicesQuery, fullMatchServices); // Filter on service description containsUserInput = service => service.Description.ContainsWord(searchInputText); containsAnySynonym = service => synonyms.AnyWord(service.Description); ApplyFullTextFilter(containsUserInput, containsAnySynonym, synonyms, fullMatchServicesQuery, fullMatchServices); // More than one word in search input if (moreThan1SearchInputWord) { var splitWordSynonyms = _context.SynonymWords .Include(sw => sw.Group) .ThenInclude(sg => sg.SynonymWords) .AsEnumerable() .Where(sw => splitWords.Any(spw => sw.Word.ToLower().Contains(spw))) .SelectMany(sw => sw.Group.SynonymWords) .Select(sw => sw.Word.ToLower()) .ToHashSet(); // Filter words on service organisation name Predicate <Service> containsSplitUserInput = service => splitWords.Any(spw => service.Organization.Name.ToLower().Contains(spw)); ApplySplitMatchFilter(containsSplitUserInput, splitMatchServicesQuery, demographicTaxonomies, categoryTaxonomies, splitMatchServices, fullMatchServices); // Filter words on service name containsSplitUserInput = service => splitWords.Any(spw => service.Name.ToLower().Contains(spw)); ApplySplitMatchFilter(containsSplitUserInput, splitMatchServicesQuery, demographicTaxonomies, categoryTaxonomies, splitMatchServices, fullMatchServices); // Filter words on service description containsSplitUserInput = service => splitWords.AnyWord(service.Description); ApplySplitMatchFilter(containsSplitUserInput, splitMatchServicesQuery, demographicTaxonomies, categoryTaxonomies, splitMatchServices, fullMatchServices); // Filter synonyms on organisation name Predicate <Service> containsAnySplitInputSynonym = service => splitWordSynonyms.Any(spwsn => service.Organization.Name.ToLower().Contains(spwsn)); ApplySplitSynonymsMatchFilter(containsAnySplitInputSynonym, splitMatchServicesQuery, splitWordSynonyms, demographicTaxonomies, categoryTaxonomies, splitMatchServices, fullMatchServices); // Filter synonyms on service name containsAnySplitInputSynonym = service => splitWordSynonyms.Any(spwsn => service.Name.ToLower().Contains(spwsn)); ApplySplitSynonymsMatchFilter(containsAnySplitInputSynonym, splitMatchServicesQuery, splitWordSynonyms, demographicTaxonomies, categoryTaxonomies, splitMatchServices, fullMatchServices); // Filter synonyms on service description containsAnySplitInputSynonym = service => splitWordSynonyms.AnyWord(service.Description); ApplySplitSynonymsMatchFilter(containsAnySplitInputSynonym, splitMatchServicesQuery, splitWordSynonyms, demographicTaxonomies, categoryTaxonomies, splitMatchServices, fullMatchServices); } } else { AddToCollection(fullMatchServices, fullMatchServicesQuery.Select(s => s.ToDomain()).ToList()); } return(new SearchServiceGatewayResult(fullMatchServices, splitMatchServices)); }