From c94ea4a62428528dc8ebaf178b6d8f778b23246f Mon Sep 17 00:00:00 2001 From: Harrison Deng Date: Thu, 5 Aug 2021 01:22:19 -0500 Subject: [PATCH] Added primitive search mechanism in backend. Began implementing search mechanism for frontend. --- .../Props.Shop/Adafruit/AdafruitShop.cs | 31 ++-- .../Adafruit/Api/IProductListingManager.cs | 16 ++ .../Props.Shop/Adafruit/Api/ListingsParser.cs | 47 ++--- .../Adafruit/Api/LiveProductListingManager.cs | 77 ++++++++ .../Adafruit/Api/ProductListingManager.cs | 58 ------ .../Props.Shop/Adafruit/Api/SearchManager.cs | 72 ++++++++ .../Props.Shop/Adafruit/Configuration.cs | 5 + .../Adafruit/Options/SimilarityOption.cs | 35 ---- .../Adafruit/Props.Shop.Adafruit.csproj | 1 + Props-Modules/Props.Shop/Ebay/EbayShop.cs | 16 +- .../Props.Shop/Ebay/Options/SandboxOption.cs | 35 ---- Props-Modules/Props.Shop/Framework/IShop.cs | 6 +- .../Props.Shop/Framework/ProductListing.cs | 2 +- .../Adafruit.Tests/AdafruitShopTest.cs | 23 +++ .../Api/FakeProductListingManager.cs | 87 +++++++++ .../Adafruit.Tests/Api/ListingParserTest.cs | 6 +- .../Api/ProductListingManagerTest.cs | 25 --- .../Adafruit.Tests/Api/SearchManagerTest.cs | 20 ++ Props/.vscode/settings.json | 1 + .../Areas/Identity/Pages/Account/Login.cshtml | 18 +- .../Pages/Account/Manage/_ManageNav.cshtml | 21 ++- .../Pages/Account/RegisterConfirmation.cshtml | 8 +- Props/Content/search.json | 16 ++ Props/Controllers/SearchController.cs | 18 +- Props/Controllers/SearchOutlineController.cs | 37 ++++ Props/Data/ApplicationDbContext.cs | 12 +- ... 20210805055109_InitialCreate.Designer.cs} | 123 ++++++++++++- ...ate.cs => 20210805055109_InitialCreate.cs} | 146 +++++++++++++-- .../ApplicationDbContextModelSnapshot.cs | 121 +++++++++++- Props/Extensions/ProductListingExtensions.cs | 14 ++ Props/Models/Search/ProductListingInfo.cs | 2 +- Props/Models/Search/QueryWordInfo.cs | 34 ++++ Props/Models/Search/SearchOutline.cs | 23 ++- Props/Models/User/ApplicationUser.cs | 9 +- Props/Models/User/SearchOutlinePreferences.cs | 37 ++++ Props/Options/ModulesOptions.cs | 1 - Props/Options/SearchOptions.cs | 9 + Props/Pages/Search.cshtml | 172 +++++++++++++++--- Props/Pages/Search.cshtml.cs | 47 ++++- Props/Pages/Shared/_Layout.cshtml | 18 +- Props/Props.csproj | 3 + Props/Services/Modules/IMetricsManager.cs | 18 ++ Props/Services/Modules/ISearchManager.cs | 12 ++ Props/Services/Modules/IShopManager.cs | 8 +- Props/Services/Modules/LiveMetricsManager.cs | 85 +++++++++ Props/Services/Modules/LiveSearchManager.cs | 58 ++++++ ...alShopManager.cs => ModularShopManager.cs} | 72 +++++--- Props/Startup.cs | 4 +- Props/appsettings.Development.json | 9 +- Props/appsettings.json | 4 +- Props/assets/js/specific/search.js | 153 ++++++---------- Props/assets/styles/site.scss | 91 +++++++-- Props/package-lock.json | 59 +++--- Props/package.json | 4 +- Props/shops/Props.Shop.Adafruit.deps.json | 84 +++++++++ Props/shops/Props.Shop.Adafruit.dll | Bin 18432 -> 18944 bytes 56 files changed, 1623 insertions(+), 490 deletions(-) create mode 100644 Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs create mode 100644 Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs delete mode 100644 Props-Modules/Props.Shop/Adafruit/Api/ProductListingManager.cs create mode 100644 Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs delete mode 100644 Props-Modules/Props.Shop/Adafruit/Options/SimilarityOption.cs delete mode 100644 Props-Modules/Props.Shop/Ebay/Options/SandboxOption.cs create mode 100644 Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs create mode 100644 Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs delete mode 100644 Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ProductListingManagerTest.cs create mode 100644 Props-Modules/test/Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs create mode 100644 Props/Content/search.json create mode 100644 Props/Controllers/SearchOutlineController.cs rename Props/Data/Migrations/{20210724073427_InitialCreate.Designer.cs => 20210805055109_InitialCreate.Designer.cs} (77%) rename Props/Data/Migrations/{20210724073427_InitialCreate.cs => 20210805055109_InitialCreate.cs} (70%) create mode 100644 Props/Extensions/ProductListingExtensions.cs create mode 100644 Props/Models/Search/QueryWordInfo.cs create mode 100644 Props/Models/User/SearchOutlinePreferences.cs create mode 100644 Props/Options/SearchOptions.cs create mode 100644 Props/Services/Modules/IMetricsManager.cs create mode 100644 Props/Services/Modules/ISearchManager.cs create mode 100644 Props/Services/Modules/LiveMetricsManager.cs create mode 100644 Props/Services/Modules/LiveSearchManager.cs rename Props/Services/Modules/{LocalShopManager.cs => ModularShopManager.cs} (66%) create mode 100644 Props/shops/Props.Shop.Adafruit.deps.json diff --git a/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs b/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs index 5d6fd5a..650f8a5 100644 --- a/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs +++ b/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text.Json; +using System.Threading.Tasks; using Props.Shop.Adafruit.Api; using Props.Shop.Framework; @@ -9,7 +11,7 @@ namespace Props.Shop.Adafruit { public class AdafruitShop : IShop { - private ProductListingManager productListingManager; + private SearchManager searchManager; private Configuration configuration; private HttpClient http; private bool disposedValue; @@ -27,24 +29,24 @@ namespace Props.Shop.Adafruit false, true ); - - public byte[] GetDataForPersistence() - { - return JsonSerializer.SerializeToUtf8Bytes(configuration); - } - - public IEnumerable Initialize(byte[] data) + public void Initialize(string workspaceDir) { http = new HttpClient(); - http.BaseAddress = new Uri("http://www.adafruit.com/api"); - configuration = JsonSerializer.Deserialize(data); - this.productListingManager = new ProductListingManager(); - return null; + http.BaseAddress = new Uri("http://www.adafruit.com/api/"); + configuration = new Configuration(); // TODO Implement config persistence. } - public IAsyncEnumerable Search(string query, Filters filters) + public async Task InitializeAsync(string workspaceDir) { - return productListingManager.Search(query, configuration.Similarity, http); + ProductListingManager productListingManager = new ProductListingManager(http); + this.searchManager = new SearchManager(productListingManager, configuration.Similarity); + await productListingManager.DownloadListings(); + productListingManager.StartUpdateTimer(); + } + + public IEnumerable Search(string query, Filters filters) + { + return searchManager.Search(query); } protected virtual void Dispose(bool disposing) @@ -54,6 +56,7 @@ namespace Props.Shop.Adafruit if (disposing) { http.Dispose(); + searchManager.Dispose(); } disposedValue = true; } diff --git a/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs new file mode 100644 index 0000000..5caeaca --- /dev/null +++ b/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Props.Shop.Framework; + +namespace Props.Shop.Adafruit.Api +{ + public interface IProductListingManager : IDisposable + { + public event EventHandler DataUpdateEvent; + public IDictionary> ActiveListings { get; } + public Task DownloadListings(); + public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5); + public void StopUpdateTimer(); + } +} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Api/ListingsParser.cs b/Props-Modules/Props.Shop/Adafruit/Api/ListingsParser.cs index eb828f6..c454584 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/ListingsParser.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/ListingsParser.cs @@ -1,44 +1,49 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Net.Http; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Props.Shop.Framework; namespace Props.Shop.Adafruit.Api { - public class ListingsParser + public class ProductListingsParser { public IEnumerable ProductListings { get; private set; } - public ListingsParser(string json) + public void BuildProductListings(Stream stream) { - dynamic data = JArray.Parse(json); - List parsed = new List(); - foreach (dynamic item in data) + using (StreamReader streamReader = new StreamReader(stream)) { - if (item.products_discontinued == 0) + dynamic data = JArray.Load(new JsonTextReader(streamReader)); + List parsed = new List(); + foreach (dynamic item in data) { - ProductListing res = new ProductListing(); - res.Name = item.product_name; - res.LowerPrice = item.product_price; - res.UpperPrice = res.LowerPrice; - foreach (dynamic discount in item.discount_pricing) + if (item.products_discontinued == 0) { - if (discount.discounted_price < res.LowerPrice) + ProductListing res = new ProductListing(); + res.Name = item.product_name; + res.LowerPrice = item.product_price; + res.UpperPrice = res.LowerPrice; + foreach (dynamic discount in item.discount_pricing) { - res.LowerPrice = discount.discounted_price; - } - if (discount.discounted_price > res.UpperPrice) - { - res.UpperPrice = discount.discounted_price; + if (discount.discounted_price < res.LowerPrice) + { + res.LowerPrice = discount.discounted_price; + } + if (discount.discounted_price > res.UpperPrice) + { + res.UpperPrice = discount.discounted_price; + } } + res.URL = item.product_url; + res.InStock = item.product_stock > 0; + parsed.Add(res); } - res.URL = item.product_url; - res.InStock = item.product_stock > 0; - parsed.Add(res); } + ProductListings = parsed; } - ProductListings = parsed; } } } \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs new file mode 100644 index 0000000..8cfb685 --- /dev/null +++ b/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Props.Shop.Framework; + +namespace Props.Shop.Adafruit.Api +{ + public class ProductListingManager : IProductListingManager + { + public event EventHandler DataUpdateEvent; + private bool disposedValue; + private volatile Dictionary> activeListings; + public IDictionary> ActiveListings => activeListings; + private ProductListingsParser parser = new ProductListingsParser(); + private HttpClient httpClient; + private Timer updateTimer; + + public ProductListingManager(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + public async Task DownloadListings() { + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + HttpResponseMessage responseMessage = await httpClient.GetAsync("products"); + parser.BuildProductListings(responseMessage.Content.ReadAsStream()); + Dictionary> listings = new Dictionary>(); + foreach (ProductListing product in parser.ProductListings) + { + IList sameProducts = listings.GetValueOrDefault(product.Name); + if (sameProducts == null) { + sameProducts = new List(); + listings.Add(product.Name, sameProducts); + } + + sameProducts.Add(product); + } + activeListings = listings; + DataUpdateEvent?.Invoke(this, null); + } + + public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5) { + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + if (updateTimer != null) throw new InvalidOperationException("Update timer already started."); + updateTimer = new Timer(async (state) => await DownloadListings(), null, delay, period); + } + + public void StopUpdateTimer() { + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + if (updateTimer != null) throw new InvalidOperationException("Update timer not started."); + updateTimer.Dispose(); + updateTimer = null; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + updateTimer?.Dispose(); + updateTimer = null; + } + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Api/ProductListingManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/ProductListingManager.cs deleted file mode 100644 index 6305a2e..0000000 --- a/Props-Modules/Props.Shop/Adafruit/Api/ProductListingManager.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using FuzzySharp; -using FuzzySharp.Extractor; -using Props.Shop.Framework; - -namespace Props.Shop.Adafruit.Api -{ - public class ProductListingManager - { - private double minutesPerRequest; - private Dictionary> listings = new Dictionary>(); - private bool requested = false; - public DateTime TimeOfLastRequest { get; private set; } - public bool RequestReady => !requested || DateTime.Now - TimeOfLastRequest > TimeSpan.FromMinutes(minutesPerRequest); - - public ProductListingManager(int requestsPerMinute = 5) - { - this.minutesPerRequest = 1 / requestsPerMinute; - } - - public async Task RefreshListings(HttpClient http) - { - requested = true; - TimeOfLastRequest = DateTime.Now; - HttpResponseMessage response = await http.GetAsync("/products"); - SetListingsData(await response.Content.ReadAsStringAsync()); - } - - public void SetListingsData(string data) - { - ListingsParser listingsParser = new ListingsParser(data); - foreach (ProductListing listing in listingsParser.ProductListings) - { - List similar = listings.GetValueOrDefault(listing.Name, new List()); - similar.Add(listing); - listings[listing.Name] = similar; - } - } - - public async IAsyncEnumerable Search(string query, float similarity, HttpClient httpClient = null) - { - if (RequestReady && httpClient != null) await RefreshListings(httpClient); - IEnumerable> resultNames = Process.ExtractAll(query, listings.Keys, cutoff: (int)similarity * 100); - foreach (ExtractedResult resultName in resultNames) - { - foreach (ProductListing product in listings[resultName.Value]) - { - yield return product; - } - } - } - - } -} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs new file mode 100644 index 0000000..5b57379 --- /dev/null +++ b/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using FuzzySharp; +using FuzzySharp.Extractor; +using Props.Shop.Framework; + +namespace Props.Shop.Adafruit.Api +{ + public class SearchManager : IDisposable + { + public float Similarity { get; set; } + private readonly object searchLock = new object(); + private IDictionary> searched; + private IProductListingManager listingManager; + private bool disposedValue; + + public SearchManager(IProductListingManager productListingManager, float similarity = 0.8f) + { + this.listingManager = productListingManager ?? throw new ArgumentNullException("productListingManager"); + this.Similarity = similarity; + listingManager.DataUpdateEvent += OnDataUpdate; + } + + private void OnDataUpdate(object sender, EventArgs eventArgs) + { + BuildSearchIndex(); + } + + private void BuildSearchIndex() + { + lock (searchLock) + { + searched = new Dictionary>(listingManager.ActiveListings); + } + } + + public IEnumerable Search(string query) + { + lock (searchLock) + { + foreach (ExtractedResult listingNames in Process.ExtractAll(query, searched.Keys, cutoff: (int)(Similarity * 100))) + { + foreach (ProductListing same in searched[listingNames.Value]) + { + yield return same; + } + } + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + listingManager.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Configuration.cs b/Props-Modules/Props.Shop/Adafruit/Configuration.cs index 1c5591d..06e1407 100644 --- a/Props-Modules/Props.Shop/Adafruit/Configuration.cs +++ b/Props-Modules/Props.Shop/Adafruit/Configuration.cs @@ -3,5 +3,10 @@ namespace Props.Shop.Adafruit public class Configuration { public float Similarity { get; set; } + + public Configuration() + { + Similarity = 0.8f; + } } } \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Options/SimilarityOption.cs b/Props-Modules/Props.Shop/Adafruit/Options/SimilarityOption.cs deleted file mode 100644 index ee554b2..0000000 --- a/Props-Modules/Props.Shop/Adafruit/Options/SimilarityOption.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Props.Shop.Framework; - -namespace Props.Shop.Adafruit.Options -{ - public class SimilarityOption : IOption - { - private Configuration configuration; - public string Name => "Query Similarity"; - - public string Description => "The minimum level of similarity for a listing to be returned."; - - public bool Required => true; - - public Type Type => typeof(float); - - internal SimilarityOption(Configuration configuration) - { - this.configuration = configuration; - } - - public string GetValue() - { - return configuration.Similarity.ToString(); - } - - public bool SetValue(string value) - { - float parsed; - bool success = float.TryParse(value, out parsed); - configuration.Similarity = parsed; - return success; - } - } -} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Props.Shop.Adafruit.csproj b/Props-Modules/Props.Shop/Adafruit/Props.Shop.Adafruit.csproj index 7f0c8f5..4ed8f78 100644 --- a/Props-Modules/Props.Shop/Adafruit/Props.Shop.Adafruit.csproj +++ b/Props-Modules/Props.Shop/Adafruit/Props.Shop.Adafruit.csproj @@ -7,6 +7,7 @@ + diff --git a/Props-Modules/Props.Shop/Ebay/EbayShop.cs b/Props-Modules/Props.Shop/Ebay/EbayShop.cs index b9d0da5..ad3a5f0 100644 --- a/Props-Modules/Props.Shop/Ebay/EbayShop.cs +++ b/Props-Modules/Props.Shop/Ebay/EbayShop.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; +using System.Threading.Tasks; using Props.Shop.Framework; namespace Props.Shop.Ebay @@ -29,13 +30,16 @@ namespace Props.Shop.Ebay private HttpClient httpClient; - public IEnumerable Initialize(byte[] data) + public void Initialize(string workspaceDir) { httpClient = new HttpClient(); - configuration = JsonSerializer.Deserialize(data); - return new List() { - new SandboxOption(configuration), - }; + configuration = new Configuration(); // TODO: Implement config persistence. + } + + + public Task InitializeAsync(string workspaceDir) + { + throw new NotImplementedException(); } protected virtual void Dispose(bool disposing) @@ -61,7 +65,7 @@ namespace Props.Shop.Ebay return JsonSerializer.SerializeToUtf8Bytes(configuration); } - public IAsyncEnumerable Search(string query, Filters filters) + public IEnumerable Search(string query, Filters filters) { // TODO: Implement the search system. throw new NotImplementedException(); diff --git a/Props-Modules/Props.Shop/Ebay/Options/SandboxOption.cs b/Props-Modules/Props.Shop/Ebay/Options/SandboxOption.cs deleted file mode 100644 index a7cf53d..0000000 --- a/Props-Modules/Props.Shop/Ebay/Options/SandboxOption.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Props.Shop.Framework; - -namespace Props.Shop.Ebay -{ - public class SandboxOption : IOption - { - private Configuration configuration; - public string Name => "Ebay Sandbox"; - - public string Description => "For development purposes, Ebay Sandbox allows use of Ebay APIs (with exceptions) in a sandbox environment before applying for production use."; - - public bool Required => true; - - public Type Type => typeof(bool); - - internal SandboxOption(Configuration configuration) - { - this.configuration = configuration; - } - - public string GetValue() - { - return configuration.Sandbox.ToString(); - } - - public bool SetValue(string value) - { - bool sandbox = false; - bool res = bool.TryParse(value, out sandbox); - configuration.Sandbox = sandbox; - return res; - } - } -} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Framework/IShop.cs b/Props-Modules/Props.Shop/Framework/IShop.cs index 02043b1..3e60f9d 100644 --- a/Props-Modules/Props.Shop/Framework/IShop.cs +++ b/Props-Modules/Props.Shop/Framework/IShop.cs @@ -12,10 +12,10 @@ namespace Props.Shop.Framework string ShopDescription { get; } string ShopModuleAuthor { get; } - public IAsyncEnumerable Search(string query, Filters filters); + public IEnumerable Search(string query, Filters filters); - IEnumerable Initialize(byte[] data); + void Initialize(string workspaceDir); + Task InitializeAsync(string workspaceDir); public SupportedFeatures SupportedFeatures { get; } - public byte[] GetDataForPersistence(); } } \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Framework/ProductListing.cs b/Props-Modules/Props.Shop/Framework/ProductListing.cs index 3b9111b..bb9f356 100644 --- a/Props-Modules/Props.Shop/Framework/ProductListing.cs +++ b/Props-Modules/Props.Shop/Framework/ProductListing.cs @@ -1,6 +1,6 @@ namespace Props.Shop.Framework { - public struct ProductListing + public class ProductListing { public float LowerPrice { get; set; } public float UpperPrice { get; set; } diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs new file mode 100644 index 0000000..46f8e1c --- /dev/null +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Props.Shop.Framework; +using Xunit; + +namespace Props.Shop.Adafruit.Tests +{ + public class AdafruitShopTest + { + [Fact] + public async Task TestSearch() { + AdafruitShop mockAdafruitShop = new AdafruitShop(); + mockAdafruitShop.Initialize(null); + await mockAdafruitShop.InitializeAsync(null); + int count = 0; + foreach (ProductListing listing in mockAdafruitShop.Search("raspberry pi", new Filters())) + { + count += 1; + } + Assert.True(count > 0); + } + } +} \ No newline at end of file diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs new file mode 100644 index 0000000..b7e3ac8 --- /dev/null +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Props.Shop.Adafruit.Api; +using Props.Shop.Framework; + +namespace Props.Shop.Adafruit.Tests.Api +{ + public class FakeProductListingManager : IProductListingManager + { + private ProductListingsParser parser; + private Timer updateTimer; + private bool disposedValue; + + private volatile Dictionary> activeListings; + + public IDictionary> ActiveListings => activeListings; + + public event EventHandler DataUpdateEvent; + + public FakeProductListingManager() + { + parser = new ProductListingsParser(); + } + + public Task DownloadListings() + { + if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager"); + using (Stream stream = File.OpenRead("./Assets/products.json")) + { + parser.BuildProductListings(stream); + } + + Dictionary> results = new Dictionary>(); + foreach (ProductListing product in parser.ProductListings) + { + IList sameProducts = results.GetValueOrDefault(product.Name); + if (sameProducts == null) { + sameProducts = new List(); + results.Add(product.Name, sameProducts); + } + + sameProducts.Add(product); + } + activeListings = results; + DataUpdateEvent?.Invoke(this, null); + return Task.CompletedTask; + } + + public void StartUpdateTimer(int delay = 300000, int period = 300000) + { + if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager"); + if (updateTimer != null) throw new InvalidOperationException("Update timer already started."); + updateTimer = new Timer((state) => DownloadListings(), null, delay, period); + } + + public void StopUpdateTimer() + { + if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager"); + if (updateTimer == null) throw new InvalidOperationException("Update timer not started."); + updateTimer.Dispose(); + updateTimer = null; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + updateTimer?.Dispose(); + updateTimer = null; + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ListingParserTest.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ListingParserTest.cs index 83bd890..108bdfe 100644 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ListingParserTest.cs +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ListingParserTest.cs @@ -10,7 +10,11 @@ namespace Props.Shop.Adafruit.Tests [Fact] public void TestParsing() { - ListingsParser mockParser = new ListingsParser(File.ReadAllText("./Assets/products.json")); + ProductListingsParser mockParser = new ProductListingsParser(); + using (Stream stream = File.OpenRead("./Assets/products.json")) + { + mockParser.BuildProductListings(stream); + } Assert.NotEmpty(mockParser.ProductListings); } } diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ProductListingManagerTest.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ProductListingManagerTest.cs deleted file mode 100644 index 0417170..0000000 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ProductListingManagerTest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using Props.Shop.Adafruit.Api; -using Props.Shop.Framework; -using Xunit; - -namespace Props.Shop.Adafruit.Tests.Api -{ - public class ProductListingManagerTest - { - [Fact] - public async Task TestSearch() - { - ProductListingManager mockProductListingManager = new ProductListingManager(); - mockProductListingManager.SetListingsData(File.ReadAllText("./Assets/products.json")); - List results = new List(); - await foreach (ProductListing item in mockProductListingManager.Search("arduino", 0.5f)) - { - results.Add(item); - } - Assert.NotEmpty(results); - } - } -} \ No newline at end of file diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs new file mode 100644 index 0000000..a733d03 --- /dev/null +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Props.Shop.Adafruit.Api; +using Xunit; + +namespace Props.Shop.Adafruit.Tests.Api +{ + public class SearchManagerTest + { + [Fact] + public async Task SearchTest() + { + FakeProductListingManager stubProductListingManager = new FakeProductListingManager(); + SearchManager searchManager = new SearchManager(stubProductListingManager); + await stubProductListingManager.DownloadListings(); + searchManager.Similarity = 0.8f; + Assert.NotEmpty(searchManager.Search("Raspberry Pi")); + searchManager.Dispose(); + } + } +} \ No newline at end of file diff --git a/Props/.vscode/settings.json b/Props/.vscode/settings.json index bfd3fdb..312968f 100644 --- a/Props/.vscode/settings.json +++ b/Props/.vscode/settings.json @@ -5,4 +5,5 @@ ], "todo-tree.regex.regex": "((//|#|