diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..80cac42 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.cs] +# RCS1090: Add call to 'ConfigureAwait' (or vice versa) +dotnet_diagnostic.RCS1090.severity = suggestion diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index fc24d22..5661c6d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - name: Restore dependencies run: dotnet restore --runtime "win-x64" - name: Build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80ee6e4..c609fea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - name: Restore dependencies run: | dotnet nuget add source https://nuget.pkg.github.com/hwndmaster/index.json -n github -u hwndmaster -p ${{ secrets.PRIVATE_PKG_TOKEN }} --store-password-in-clear-text diff --git a/.vscode/project-words.txt b/.vscode/project-words.txt new file mode 100644 index 0000000..dce80f3 --- /dev/null +++ b/.vscode/project-words.txt @@ -0,0 +1,2 @@ +Xunit +Hardcodet diff --git a/.vscode/settings.json b/.vscode/settings.json index a92948c..f0a9562 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,12 @@ "lcov.info" ], "dotnet-test-explorer.autoWatch": true, - "editor.tabSize": 4 + "editor.tabSize": 4, + "cSpell.customDictionaries": { + "project-words": { + "name": "project-words", + "path": "${workspaceRoot}/.vscode/project-words.txt", + "addWords": true + } + } } diff --git a/Directory.Build.targets b/Directory.Build.targets index 3fa4f3e..c040a70 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,29 +1,33 @@ + + enable + + - - - - - + + + + + - - - + + + - - - + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PriceChecker.Core.Tests/PriceChecker.Core.Tests.csproj b/PriceChecker.Core.Tests/PriceChecker.Core.Tests.csproj index 1e5f9b7..2c99684 100644 --- a/PriceChecker.Core.Tests/PriceChecker.Core.Tests.csproj +++ b/PriceChecker.Core.Tests/PriceChecker.Core.Tests.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 Genius.PriceChecker.Core.Tests false @@ -12,7 +12,7 @@ - ..\..\atom\Atom.Infrastructure\bin\Debug\net5.0\Genius.Atom.Infrastructure.dll + ..\..\atom\Atom.Infrastructure\bin\Debug\net6.0\Genius.Atom.Infrastructure.dll diff --git a/PriceChecker.Core.Tests/Repositories/AgentRepositoryTests.cs b/PriceChecker.Core.Tests/Repositories/AgentRepositoryTests.cs index a83761e..5088f76 100644 --- a/PriceChecker.Core.Tests/Repositories/AgentRepositoryTests.cs +++ b/PriceChecker.Core.Tests/Repositories/AgentRepositoryTests.cs @@ -11,85 +11,84 @@ using Moq; using Xunit; -namespace Genius.PriceChecker.Core.Tests.Repositories +namespace Genius.PriceChecker.Core.Tests.Repositories; + +public class AgentRepositoryTests { - public class AgentRepositoryTests + private readonly AgentRepository _sut; + private readonly Fixture _fixture = new(); + private readonly Mock _eventBusMock = new(); + private readonly Mock _persisterMock = new(); + + private readonly List _agents = new(); + + public AgentRepositoryTests() + { + _agents = _fixture.CreateMany().ToList(); + + _persisterMock.Setup(x => x.LoadCollection(It.IsAny())) + .Returns(_agents.ToArray()); + + _sut = new AgentRepository(_eventBusMock.Object, _persisterMock.Object, + Mock.Of>()); + + _sut.GetAll(); // To trigger the initializer + } + + [Fact] + public void FindById__Returns_appropriate_agent() + { + // Arrange + var agentToFind = _agents[1]; + + // Act + var result = _sut.FindById(agentToFind.Id); + + // Verify + Assert.Equal(agentToFind, result); + } + + [Fact] + public void Delete__Removes_appripriate_agent() + { + // Arrange + var agentToDelete = _agents[1]; + + // Act + _sut.Delete(agentToDelete.Id); + + // Verify + Assert.Null(_sut.FindById(agentToDelete.Id)); + } + + [Fact] + public void Delete__When_no_agent_found__Breaks_operation() + { + // Arrange + var agentCount = _sut.GetAll().Count(); + + // Act + _sut.Delete(Guid.NewGuid()); + + // Verify + Assert.Equal(agentCount, _sut.GetAll().Count()); + } + + [Fact] + public void Store__Replaces_all_existing_agents_and_updates_cache_and_fires_event() { - private readonly AgentRepository _sut; - private readonly Fixture _fixture = new(); - private readonly Mock _eventBusMock = new(); - private readonly Mock _persisterMock = new(); - - private readonly List _agents = new(); - - public AgentRepositoryTests() - { - _agents = _fixture.CreateMany().ToList(); - - _persisterMock.Setup(x => x.LoadCollection(It.IsAny())) - .Returns(_agents.ToArray()); - - _sut = new AgentRepository(_eventBusMock.Object, _persisterMock.Object, - Mock.Of>()); - - _sut.GetAll(); // To trigger the initializer - } - - [Fact] - public void FindById__Returns_appropriate_agent() - { - // Arrange - var agentToFind = _agents[1]; - - // Act - var result = _sut.FindById(agentToFind.Id); - - // Verify - Assert.Equal(agentToFind, result); - } - - [Fact] - public void Delete__Removes_appripriate_agent() - { - // Arrange - var agentToDelete = _agents[1]; - - // Act - _sut.Delete(agentToDelete.Id); - - // Verify - Assert.Null(_sut.FindById(agentToDelete.Id)); - } - - [Fact] - public void Delete__When_no_agent_found__Breaks_operation() - { - // Arrange - var agentCount = _sut.GetAll().Count(); - - // Act - _sut.Delete(Guid.NewGuid()); - - // Verify - Assert.Equal(agentCount, _sut.GetAll().Count()); - } - - [Fact] - public void Store__Replaces_all_existing_agents_and_updates_cache_and_fires_event() - { - // Arrange - var newAgents = _fixture.CreateMany().ToArray(); - var previousAgents = _sut.GetAll().ToArray(); - - // Act - _sut.Overwrite(newAgents); - - // Verify - Assert.False(_sut.GetAll().Except(newAgents).Any()); - _persisterMock.Verify(x => x.Store(It.IsAny(), - It.Is((List p) => p.SequenceEqual(newAgents)))); - _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Entities.Count == newAgents.Length)), Times.Once); - _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Entities.Count == previousAgents.Length)), Times.Once); - } + // Arrange + var newAgents = _fixture.CreateMany().ToArray(); + var previousAgents = _sut.GetAll().ToArray(); + + // Act + _sut.Overwrite(newAgents); + + // Verify + Assert.False(_sut.GetAll().Except(newAgents).Any()); + _persisterMock.Verify(x => x.Store(It.IsAny(), + It.Is((List p) => p.SequenceEqual(newAgents)))); + _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Entities.Count == newAgents.Length)), Times.Once); + _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Entities.Count == previousAgents.Length)), Times.Once); } } diff --git a/PriceChecker.Core.Tests/Repositories/ProductRepositoryTests.cs b/PriceChecker.Core.Tests/Repositories/ProductRepositoryTests.cs index 6b9f72d..d2669db 100644 --- a/PriceChecker.Core.Tests/Repositories/ProductRepositoryTests.cs +++ b/PriceChecker.Core.Tests/Repositories/ProductRepositoryTests.cs @@ -11,162 +11,161 @@ using Moq; using Xunit; -namespace Genius.PriceChecker.Core.Tests.Repositories +namespace Genius.PriceChecker.Core.Tests.Repositories; + +public class ProductRepositoryTests { - public class ProductRepositoryTests - { - private readonly ProductRepository _sut; - private readonly Fixture _fixture = new(); - private readonly Mock _eventBusMock = new(); - private readonly Mock _persisterMock = new(); - private readonly Mock _agentQueryMock = new(); + private readonly ProductRepository _sut; + private readonly Fixture _fixture = new(); + private readonly Mock _eventBusMock = new(); + private readonly Mock _persisterMock = new(); + private readonly Mock _agentQueryMock = new(); - private readonly List _products = new(); - private readonly List _agents = new(); + private readonly List _products = new(); + private readonly List _agents = new(); - public ProductRepositoryTests() - { - _products.AddRange(Enumerable.Range(1, 3).Select(_ => CreateProduct())); - _agents = _products.SelectMany(x => x.Sources) - .Select(x => new Agent { Key = x.AgentKey }).ToList(); + public ProductRepositoryTests() + { + _products.AddRange(Enumerable.Range(1, 3).Select(_ => CreateProduct())); + _agents = _products.SelectMany(x => x.Sources) + .Select(x => new Agent { Key = x.AgentKey }).ToList(); - foreach (var agent in _agents) - _agentQueryMock.Setup(x => x.FindByKey(agent.Key)).Returns(agent); + foreach (var agent in _agents) + _agentQueryMock.Setup(x => x.FindByKey(agent.Key)).Returns(agent); - _persisterMock.Setup(x => x.LoadCollection(It.IsAny())) - .Returns(_products.ToArray()); + _persisterMock.Setup(x => x.LoadCollection(It.IsAny())) + .Returns(_products.ToArray()); - _sut = new ProductRepository(_eventBusMock.Object, _persisterMock.Object, - _agentQueryMock.Object, - Mock.Of>()); + _sut = new ProductRepository(_eventBusMock.Object, _persisterMock.Object, + _agentQueryMock.Object, + Mock.Of>()); - _sut.GetAll(); // To trigger the initializer - } + _sut.GetAll(); // To trigger the initializer + } - [Fact] - public void GetAll__Returns_all_loaded_products() - { - // Act - var result = _sut.GetAll(); + [Fact] + public void GetAll__Returns_all_loaded_products() + { + // Act + var result = _sut.GetAll(); - // Verify - Assert.Equal(_products, result); - } + // Verify + Assert.Equal(_products, result); + } - [Fact] - public void FindById__Returns_appropriate_product() - { - // Arrange - var productToFind = _products[1]; + [Fact] + public void FindById__Returns_appropriate_product() + { + // Arrange + var productToFind = _products[1]; - // Act - var result = _sut.FindById(productToFind.Id); + // Act + var result = _sut.FindById(productToFind.Id); - // Verify - Assert.Equal(productToFind, result); - } + // Verify + Assert.Equal(productToFind, result); + } - [Fact] - public void Delete__Removes_appripriate_product() - { - // Arrange - var productToDelete = _products[1]; + [Fact] + public void Delete__Removes_appripriate_product() + { + // Arrange + var productToDelete = _products[1]; - // Act - _sut.Delete(productToDelete.Id); + // Act + _sut.Delete(productToDelete.Id); - // Verify - Assert.Null(_sut.FindById(productToDelete.Id)); - } + // Verify + Assert.Null(_sut.FindById(productToDelete.Id)); + } - [Fact] - public void Delete__When_no_product_found__Breaks_operation() - { - // Arrange - var productCount = _sut.GetAll().Count(); + [Fact] + public void Delete__When_no_product_found__Breaks_operation() + { + // Arrange + var productCount = _sut.GetAll().Count(); - // Act - _sut.Delete(Guid.NewGuid()); + // Act + _sut.Delete(Guid.NewGuid()); - // Verify - Assert.Equal(productCount, _sut.GetAll().Count()); - } + // Verify + Assert.Equal(productCount, _sut.GetAll().Count()); + } - [Fact] - public void Store__For_existing_product__Saves_it_and_fires_event() - { - // Arrange - var product = _products[1]; + [Fact] + public void Store__For_existing_product__Saves_it_and_fires_event() + { + // Arrange + var product = _products[1]; - // Act - _sut.Store(product); + // Act + _sut.Store(product); - // Verify - _persisterMock.Verify(x => x.Store(It.IsAny(), - It.Is((List p) => p.SequenceEqual(_products)))); - _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Entities.First().Value == product)), Times.Once); - } + // Verify + _persisterMock.Verify(x => x.Store(It.IsAny(), + It.Is((List p) => p.SequenceEqual(_products)))); + _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Entities.First().Value == product)), Times.Once); + } - [Fact] - public void Store__For_nonexisting_product__Adds_it_and_fires_event() - { - // Arrange - var product = CreateProduct(); - var productCount = _sut.GetAll().Count(); - - // Act - _sut.Store(product); - - // Verify - var expectedProducts = _products.Concat(new [] { product }); - _persisterMock.Verify(x => x.Store(It.IsAny(), - It.Is((List p) => p.SequenceEqual(expectedProducts)))); - _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Entities.First().Value == product)), Times.Once); - Assert.Equal(productCount + 1, _sut.GetAll().Count()); - } + [Fact] + public void Store__For_nonexisting_product__Adds_it_and_fires_event() + { + // Arrange + var product = CreateProduct(); + var productCount = _sut.GetAll().Count(); + + // Act + _sut.Store(product); + + // Verify + var expectedProducts = _products.Concat(new [] { product }); + _persisterMock.Verify(x => x.Store(It.IsAny(), + It.Is((List p) => p.SequenceEqual(expectedProducts)))); + _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Entities.First().Value == product)), Times.Once); + Assert.Equal(productCount + 1, _sut.GetAll().Count()); + } - [Fact] - public void Store__When_id_is_empty__Adds_product_with_autogenerated_id() - { - // Arrange - var product = CreateProduct(); - product.Id = Guid.Empty; - var productCount = _sut.GetAll().Count(); + [Fact] + public void Store__When_id_is_empty__Adds_product_with_autogenerated_id() + { + // Arrange + var product = CreateProduct(); + product.Id = Guid.Empty; + var productCount = _sut.GetAll().Count(); - // Act - _sut.Store(product); + // Act + _sut.Store(product); - // Verify - Assert.NotEqual(Guid.Empty, product.Id); - Assert.Equal(productCount + 1, _sut.GetAll().Count()); - } + // Verify + Assert.NotEqual(Guid.Empty, product.Id); + Assert.Equal(productCount + 1, _sut.GetAll().Count()); + } - [Fact] - public void Constructor__Loads_and_fills_up_relations() - { - // Act (done in the test constructor) + [Fact] + public void Constructor__Loads_and_fills_up_relations() + { + // Act (done in the test constructor) - // Verify - foreach (var product in _products) + // Verify + foreach (var product in _products) + { + foreach (var source in product.Sources) + { + Assert.Equal(source.AgentKey, source.Agent.Key); + Assert.Equal(product, source.Product); + } + foreach (var price in product.Recent) { - foreach (var source in product.Sources) - { - Assert.Equal(source.AgentKey, source.Agent.Key); - Assert.Equal(product, source.Product); - } - foreach (var price in product.Recent) - { - Assert.Equal(product.Sources.First(x => x.Id == price.ProductSourceId), price.ProductSource); - } + Assert.Equal(product.Sources.First(x => x.Id == price.ProductSourceId), price.ProductSource); } } + } - private Product CreateProduct() - { - var product = _fixture.Create(); - foreach (var (first, second) in product.Recent.Zip(product.Sources)) - first.ProductSourceId = second.Id; - return product; - } + private Product CreateProduct() + { + var product = _fixture.Create(); + foreach (var (first, second) in product.Recent.Zip(product.Sources)) + first.ProductSourceId = second.Id; + return product; } } diff --git a/PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs b/PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs index c61ca01..4d05179 100644 --- a/PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs +++ b/PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs @@ -9,88 +9,87 @@ using Moq; using Xunit; -namespace Genius.PriceChecker.Core.Tests.Repositories +namespace Genius.PriceChecker.Core.Tests.Repositories; + +public class SettingsRepositoryTests { - public class SettingsRepositoryTests + private readonly Fixture _fixture = new(); + private readonly Mock _eventBusMock = new(); + private readonly Mock _persisterMock = new(); + + [Fact] + public void Constructor__Previous_settings_exist__Loaded() + { + // Arrange + var settings = _fixture.Create(); + + // Act + var sut = CreateSystemUnderTest(settings); + + // Verify + Assert.Equal(settings, sut.Get()); + } + + [Fact] + public void Constructor__Previous_settings_dont_exist__Loaded_default() + { + // Arrange + Settings? settings = null; + + // Act + var sut = CreateSystemUnderTest(settings); + + // Verify + var result = sut.Get(); + Assert.False(result.AutoRefreshEnabled); + Assert.Equal(1440, result.AutoRefreshMinutes); + } + + [Fact] + public void Get__returns_currently_loaded_settings() + { + // Arrange + var settings = _fixture.Create(); + var sut = CreateSystemUnderTest(settings); + + // Act + var result = sut.Get(); + + // Verify + Assert.Equal(settings, result); + } + + [Fact] + public void Store__Argument_not_provided__throws_exception() + { + // Arrange + var sut = CreateSystemUnderTest(); + + // Act & Verify + Assert.Throws(() => sut.Store(null!)); + } + + [Fact] + public void Store__Replaces_existing_settings_and_updates_cache_and_fires_event() + { + // Arrange + var sut = CreateSystemUnderTest(); + var newSettings = _fixture.Create(); + + // Act + sut.Store(newSettings); + + // Verify + Assert.Equal(newSettings, sut.Get()); + _persisterMock.Verify(x => x.Store(It.IsAny(), newSettings)); + _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Settings == newSettings)), Times.Once); + } + + private SettingsRepository CreateSystemUnderTest(Settings? settings = null) { - private readonly Fixture _fixture = new(); - private readonly Mock _eventBusMock = new(); - private readonly Mock _persisterMock = new(); - - [Fact] - public void Constructor__Previous_settings_exist__Loaded() - { - // Arrange - var settings = _fixture.Create(); - - // Act - var sut = CreateSystemUnderTest(settings); - - // Verify - Assert.Equal(settings, sut.Get()); - } - - [Fact] - public void Constructor__Previous_settings_dont_exist__Loaded_default() - { - // Arrange - Settings settings = null; - - // Act - var sut = CreateSystemUnderTest(settings); - - // Verify - var result = sut.Get(); - Assert.False(result.AutoRefreshEnabled); - Assert.Equal(1440, result.AutoRefreshMinutes); - } - - [Fact] - public void Get__returns_currently_loaded_settings() - { - // Arrange - var settings = _fixture.Create(); - var sut = CreateSystemUnderTest(settings); - - // Act - var result = sut.Get(); - - // Verify - Assert.Equal(settings, result); - } - - [Fact] - public void Store__Argument_not_provided__throws_exception() - { - // Arrange - var sut = CreateSystemUnderTest(); - - // Act & Verify - Assert.Throws(() => sut.Store(null)); - } - - [Fact] - public void Store__Replaces_existing_settings_and_updates_cache_and_fires_event() - { - // Arrange - var sut = CreateSystemUnderTest(); - var newSettings = _fixture.Create(); - - // Act - sut.Store(newSettings); - - // Verify - Assert.Equal(newSettings, sut.Get()); - _persisterMock.Verify(x => x.Store(It.IsAny(), newSettings)); - _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Settings == newSettings)), Times.Once); - } - - private SettingsRepository CreateSystemUnderTest(Settings settings = null) - { - _persisterMock.Setup(x => x.Load(It.IsAny())) - .Returns(settings); - return new SettingsRepository(_eventBusMock.Object, _persisterMock.Object, - Mock.Of>()); - } + _persisterMock.Setup(x => x.Load(It.IsAny())) + .Returns(settings!); + return new SettingsRepository(_eventBusMock.Object, _persisterMock.Object, + Mock.Of>()); } } diff --git a/PriceChecker.Core.Tests/Services/PriceSeekerTests.cs b/PriceChecker.Core.Tests/Services/PriceSeekerTests.cs index 08a17b0..a53bbf5 100644 --- a/PriceChecker.Core.Tests/Services/PriceSeekerTests.cs +++ b/PriceChecker.Core.Tests/Services/PriceSeekerTests.cs @@ -10,144 +10,143 @@ using Moq; using Xunit; -namespace Genius.PriceChecker.Core.Tests.Services +namespace Genius.PriceChecker.Core.Tests.Services; + +public class PriceSeekerTests { - public class PriceSeekerTests - { - private readonly Fixture _fixture = new(); - private readonly Mock _httpMock = new(); - private readonly Mock _fileMock = new(); - private readonly Mock> _loggerMock = new(); + private readonly Fixture _fixture = new(); + private readonly Mock _httpMock = new(); + private readonly Mock _fileMock = new(); + private readonly Mock> _loggerMock = new(); - private readonly PriceSeeker _sut; + private readonly PriceSeeker _sut; - public PriceSeekerTests() - { - _fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth: 2)); + public PriceSeekerTests() + { + _fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth: 2)); - _sut = new PriceSeeker(_httpMock.Object, _fileMock.Object, _loggerMock.Object); - } + _sut = new PriceSeeker(_httpMock.Object, _fileMock.Object, _loggerMock.Object); + } - [Fact] - public async Task SeekAsync__Happy_flow_scenario() - { - // Arrange - var product = CreateSampleProduct(); + [Fact] + public async Task SeekAsync__Happy_flow_scenario() + { + // Arrange + var product = CreateSampleProduct(); - // Act - var result = await _sut.SeekAsync(product, new CancellationToken()); + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); - // Verify - Assert.Equal(product.Sources.Length, result.Length); - _fileMock.Verify(x => x.WriteTextToFile(It.IsAny(), It.IsAny()), Times.Never); - TestHelpers.VerifyLogger(_loggerMock, LogLevel.Warning, Times.Never()); - TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error, Times.Never()); - } + // Verify + Assert.Equal(product.Sources.Length, result.Length); + _fileMock.Verify(x => x.WriteTextToFile(It.IsAny(), It.IsAny()), Times.Never); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Warning, Times.Never()); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error, Times.Never()); + } - [Fact] - public async Task SeekAsync__Content_wasnt_downloaded__Returns_null() - { - // Arrange - var product = CreateSampleProduct(sourcesCount: 1); - _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) - .ReturnsAsync((string)null); + [Fact] + public async Task SeekAsync__Content_wasnt_downloaded__Returns_null() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1); + _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) + .ReturnsAsync((string?)null); - // Act - var result = await _sut.SeekAsync(product, new CancellationToken()); + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); - // Verify - Assert.Empty(result); - } + // Verify + Assert.Empty(result); + } - [Fact] - public async Task SeekAsync__Content_not_matched_the_pattern__Returns_null_and_dumps_file() - { - // Arrange - var product = CreateSampleProduct(sourcesCount: 1); - var contentNotMatchingAnything = _fixture.Create(); - _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) - .ReturnsAsync(contentNotMatchingAnything); - - // Act - var result = await _sut.SeekAsync(product, new CancellationToken()); - - // Verify - Assert.Empty(result); - _fileMock.Verify(x => x.WriteTextToFile(It.IsAny(), It.IsAny()), Times.Once); - TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error); - } + [Fact] + public async Task SeekAsync__Content_not_matched_the_pattern__Returns_null_and_dumps_file() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1); + var contentNotMatchingAnything = _fixture.Create(); + _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) + .ReturnsAsync(contentNotMatchingAnything); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Empty(result); + _fileMock.Verify(x => x.WriteTextToFile(It.IsAny(), It.IsAny()), Times.Once); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error); + } - [Fact] - public async Task SeekAsync__DecimalDelimiter_isnt_default__Considered_in_price_parse() - { - // Arrange - var product = CreateSampleProduct(sourcesCount: 1, delimiter: ';'); + [Fact] + public async Task SeekAsync__DecimalDelimiter_isnt_default__Considered_in_price_parse() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1, delimiter: ';'); - // Act - var result = await _sut.SeekAsync(product, new CancellationToken()); + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); - // Verify - Assert.Single(result); - Assert.NotEqual(0, result[0].Price); - Assert.NotEqual((int)result[0].Price, result[0].Price); // Check if it is decimal - } + // Verify + Assert.Single(result); + Assert.NotEqual(0, result[0].Price); + Assert.NotEqual((int)result[0].Price, result[0].Price); // Check if it is decimal + } - [Fact] - public async Task SeekAsync__Price_is_invalid__Returns_null() - { - // Arrange - var product = CreateSampleProduct(sourcesCount: 1); - var contentPriceInvalid = "`0`"; - _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) - .ReturnsAsync(contentPriceInvalid); - - // Act - var result = await _sut.SeekAsync(product, new CancellationToken()); - - // Verify - Assert.Empty(result); - TestHelpers.VerifyLogger(_loggerMock, LogLevel.Warning); - } + [Fact] + public async Task SeekAsync__Price_is_invalid__Returns_null() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1); + var contentPriceInvalid = "`0`"; + _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) + .ReturnsAsync(contentPriceInvalid); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Empty(result); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Warning); + } - [Fact] - public async Task SeekAsync__Price_isnt_convertible__Returns_null() - { - // Arrange - var product = CreateSampleProduct(sourcesCount: 1); - var contentPriceInvalid = "`not-a-number`"; - _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) - .ReturnsAsync(contentPriceInvalid); - - // Act - var result = await _sut.SeekAsync(product, new CancellationToken()); - - // Verify - Assert.Empty(result); - TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error); - } + [Fact] + public async Task SeekAsync__Price_isnt_convertible__Returns_null() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1); + var contentPriceInvalid = "`not-a-number`"; + _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) + .ReturnsAsync(contentPriceInvalid); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Empty(result); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error); + } - private Product CreateSampleProduct(int sourcesCount = 3, char delimiter = '.') + private Product CreateSampleProduct(int sourcesCount = 3, char delimiter = '.') + { + var product = _fixture.Build() + .With(x => x.Sources, _fixture.CreateMany(sourcesCount).ToArray()) + .Create(); + foreach (var productSource in product.Sources) { - var product = _fixture.Build() - .With(x => x.Sources, _fixture.CreateMany(sourcesCount).ToArray()) + productSource.Agent = _fixture.Build() + .With(x => x.Url, _fixture.Create() + "{0}") + .With(x => x.PricePattern, $@"`(?[\d\{delimiter}]+)`") + .With(x => x.DecimalDelimiter, delimiter) .Create(); - foreach (var productSource in product.Sources) - { - productSource.Agent = _fixture.Build() - .With(x => x.Url, _fixture.Create() + "{0}") - .With(x => x.PricePattern, $@"`(?[\d\{delimiter}]+)`") - .With(x => x.DecimalDelimiter, delimiter) - .Create(); - - var priceDec = _fixture.Create(); - var priceFlt = _fixture.Create(); - var content = $"{_fixture.Create()}`{priceDec}{delimiter}{priceFlt}`{_fixture.Create()}"; - - _httpMock.Setup(x => x.DownloadContent( - It.Is(url => url == string.Format(productSource.Agent.Url, productSource.AgentArgument)), It.IsAny())) - .ReturnsAsync(content); - } - return product; + + var priceDec = _fixture.Create(); + var priceFlt = _fixture.Create(); + var content = $"{_fixture.Create()}`{priceDec}{delimiter}{priceFlt}`{_fixture.Create()}"; + + _httpMock.Setup(x => x.DownloadContent( + It.Is(url => url == string.Format(productSource.Agent.Url, productSource.AgentArgument)), It.IsAny())) + .ReturnsAsync(content); } + return product; } } diff --git a/PriceChecker.Core.Tests/TestHelpers.cs b/PriceChecker.Core.Tests/TestHelpers.cs index 4d65596..f97daaf 100644 --- a/PriceChecker.Core.Tests/TestHelpers.cs +++ b/PriceChecker.Core.Tests/TestHelpers.cs @@ -2,19 +2,18 @@ using Microsoft.Extensions.Logging; using Moq; -namespace Genius.PriceChecker.Core.Tests +namespace Genius.PriceChecker.Core.Tests; + +public static class TestHelpers { - public static class TestHelpers + public static void VerifyLogger(Mock> loggerMock, LogLevel logLevel, Times? times = null) { - public static void VerifyLogger(Mock> loggerMock, LogLevel logLevel, Times? times = null) - { - if (times == null) - times = Times.Once(); - loggerMock.Verify(x => x.Log(logLevel, - It.IsAny(), - It.IsAny(), - It.IsAny(), - (Func) It.IsAny()), times.Value); - } + if (times == null) + times = Times.Once(); + loggerMock.Verify(x => x.Log(logLevel, + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func) It.IsAny()), times.Value); } -} \ No newline at end of file +} diff --git a/PriceChecker.Core/CommandHandlers/AgentDeleteCommandHandler.cs b/PriceChecker.Core/CommandHandlers/AgentDeleteCommandHandler.cs index 4c15966..0ac499b 100644 --- a/PriceChecker.Core/CommandHandlers/AgentDeleteCommandHandler.cs +++ b/PriceChecker.Core/CommandHandlers/AgentDeleteCommandHandler.cs @@ -5,26 +5,25 @@ using Genius.PriceChecker.Core.Messages; using Genius.PriceChecker.Core.Repositories; -namespace Genius.PriceChecker.Core.CommandHandlers +namespace Genius.PriceChecker.Core.CommandHandlers; + +internal sealed class AgentDeleteCommandHandler : ICommandHandler { - internal sealed class AgentDeleteCommandHandler : ICommandHandler - { - private readonly IAgentRepository _agentRepo; - private readonly IEventBus _eventBus; + private readonly IAgentRepository _agentRepo; + private readonly IEventBus _eventBus; - public AgentDeleteCommandHandler(IAgentRepository agentRepo, IEventBus eventBus) - { - _agentRepo = agentRepo; - _eventBus = eventBus; - } + public AgentDeleteCommandHandler(IAgentRepository agentRepo, IEventBus eventBus) + { + _agentRepo = agentRepo; + _eventBus = eventBus; + } - public Task ProcessAsync(AgentDeleteCommand command) - { - _agentRepo.Delete(command.AgentId); + public Task ProcessAsync(AgentDeleteCommand command) + { + _agentRepo.Delete(command.AgentId); - _eventBus.Publish(new AgentsAffectedEvent()); + _eventBus.Publish(new AgentsAffectedEvent()); - return Task.CompletedTask; - } + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/PriceChecker.Core/CommandHandlers/AgentsStoreWithOverwriteCommandHandler.cs b/PriceChecker.Core/CommandHandlers/AgentsStoreWithOverwriteCommandHandler.cs index 56d6b99..cc31627 100644 --- a/PriceChecker.Core/CommandHandlers/AgentsStoreWithOverwriteCommandHandler.cs +++ b/PriceChecker.Core/CommandHandlers/AgentsStoreWithOverwriteCommandHandler.cs @@ -5,26 +5,25 @@ using Genius.PriceChecker.Core.Messages; using Genius.PriceChecker.Core.Repositories; -namespace Genius.PriceChecker.Core.CommandHandlers +namespace Genius.PriceChecker.Core.CommandHandlers; + +internal sealed class AgentsStoreWithOverwriteCommandHandler : ICommandHandler { - internal sealed class AgentsStoreWithOverwriteCommandHandler : ICommandHandler - { - private readonly IAgentRepository _agentRepo; - private readonly IEventBus _eventBus; + private readonly IAgentRepository _agentRepo; + private readonly IEventBus _eventBus; - public AgentsStoreWithOverwriteCommandHandler(IAgentRepository agentRepo, IEventBus eventBus) - { - _agentRepo = agentRepo; - _eventBus = eventBus; - } + public AgentsStoreWithOverwriteCommandHandler(IAgentRepository agentRepo, IEventBus eventBus) + { + _agentRepo = agentRepo; + _eventBus = eventBus; + } - public Task ProcessAsync(AgentsStoreWithOverwriteCommand command) - { - _agentRepo.Overwrite(command.Agents); + public Task ProcessAsync(AgentsStoreWithOverwriteCommand command) + { + _agentRepo.Overwrite(command.Agents); - _eventBus.Publish(new AgentsAffectedEvent()); + _eventBus.Publish(new AgentsAffectedEvent()); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/PriceChecker.Core/CommandHandlers/ProductCreateOrUpdateCommandHandler.cs b/PriceChecker.Core/CommandHandlers/ProductCreateOrUpdateCommandHandler.cs index 387ee0a..66f8797 100644 --- a/PriceChecker.Core/CommandHandlers/ProductCreateOrUpdateCommandHandler.cs +++ b/PriceChecker.Core/CommandHandlers/ProductCreateOrUpdateCommandHandler.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Genius.Atom.Infrastructure.Commands; using Genius.Atom.Infrastructure.Events; @@ -8,51 +7,52 @@ using Genius.PriceChecker.Core.Models; using Genius.PriceChecker.Core.Repositories; -namespace Genius.PriceChecker.Core.CommandHandlers +namespace Genius.PriceChecker.Core.CommandHandlers; + +internal sealed class ProductCreateOrUpdateCommandHandler: + ICommandHandler, + ICommandHandler { - internal sealed class ProductCreateOrUpdateCommandHandler: - ICommandHandler, - ICommandHandler + private readonly IProductRepository _productRepo; + private readonly IProductQueryService _productQuery; + private readonly IEventBus _eventBus; + + public ProductCreateOrUpdateCommandHandler(IProductRepository productRepo, IProductQueryService productQuery, IEventBus eventBus) + { + _productRepo = productRepo; + _productQuery = productQuery; + _eventBus = eventBus; + } + + public Task ProcessAsync(ProductCreateCommand command) + { + var product = new Product(); + UpdateProperties(product, command); + _productRepo.Store(product); + + _eventBus.Publish(new ProductsAffectedEvent()); + + return Task.FromResult(product.Id); + } + + public Task ProcessAsync(ProductUpdateCommand command) + { + var product = _productQuery.FindById(command.ProductId); + Guard.AgainstNull(product, nameof(product)); + + UpdateProperties(product, command); + _productRepo.Store(product); + + _eventBus.Publish(new ProductsAffectedEvent()); + + return Task.CompletedTask; + } + + private static void UpdateProperties(Product product, ProductUpdatableData command) { - private readonly IProductRepository _productRepo; - private readonly IProductQueryService _productQuery; - private readonly IEventBus _eventBus; - - public ProductCreateOrUpdateCommandHandler(IProductRepository productRepo, IProductQueryService productQuery, IEventBus eventBus) - { - _productRepo = productRepo; - _productQuery = productQuery; - _eventBus = eventBus; - } - - public Task ProcessAsync(ProductCreateCommand command) - { - var product = new Product(); - UpdateProperties(product, command); - _productRepo.Store(product); - - _eventBus.Publish(new ProductsAffectedEvent()); - - return Task.FromResult(product.Id); - } - - public Task ProcessAsync(ProductUpdateCommand command) - { - var product = _productQuery.FindById(command.ProductId); - UpdateProperties(product, command); - _productRepo.Store(product); - - _eventBus.Publish(new ProductsAffectedEvent()); - - return Task.CompletedTask; - } - - private static void UpdateProperties(Product product, ProductUpdatableData command) - { - product.Name = command.Name; - product.Category = command.Category; - product.Description = command.Description; - product.Sources = command.Sources; - } + product.Name = command.Name; + product.Category = command.Category; + product.Description = command.Description; + product.Sources = command.Sources; } -} \ No newline at end of file +} diff --git a/PriceChecker.Core/CommandHandlers/ProductDeleteCommandHandler.cs b/PriceChecker.Core/CommandHandlers/ProductDeleteCommandHandler.cs index 9c65fd3..b9994d4 100644 --- a/PriceChecker.Core/CommandHandlers/ProductDeleteCommandHandler.cs +++ b/PriceChecker.Core/CommandHandlers/ProductDeleteCommandHandler.cs @@ -5,26 +5,25 @@ using Genius.PriceChecker.Core.Messages; using Genius.PriceChecker.Core.Repositories; -namespace Genius.PriceChecker.Core.CommandHandlers +namespace Genius.PriceChecker.Core.CommandHandlers; + +internal sealed class ProductDeleteCommandHandler : ICommandHandler { - internal sealed class ProductDeleteCommandHandler : ICommandHandler - { - private readonly IProductRepository _productRepo; - private readonly IEventBus _eventBus; + private readonly IProductRepository _productRepo; + private readonly IEventBus _eventBus; - public ProductDeleteCommandHandler(IProductRepository productRepo, IEventBus eventBus) - { - _productRepo = productRepo; - _eventBus = eventBus; - } + public ProductDeleteCommandHandler(IProductRepository productRepo, IEventBus eventBus) + { + _productRepo = productRepo; + _eventBus = eventBus; + } - public Task ProcessAsync(ProductDeleteCommand command) - { - _productRepo.Delete(command.ProductId); + public Task ProcessAsync(ProductDeleteCommand command) + { + _productRepo.Delete(command.ProductId); - _eventBus.Publish(new ProductsAffectedEvent()); + _eventBus.Publish(new ProductsAffectedEvent()); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/PriceChecker.Core/CommandHandlers/ProductDropPricesCommandHandler.cs b/PriceChecker.Core/CommandHandlers/ProductDropPricesCommandHandler.cs index d402542..b684f39 100644 --- a/PriceChecker.Core/CommandHandlers/ProductDropPricesCommandHandler.cs +++ b/PriceChecker.Core/CommandHandlers/ProductDropPricesCommandHandler.cs @@ -1,33 +1,32 @@ -using System; using System.Threading.Tasks; using Genius.Atom.Infrastructure.Commands; using Genius.PriceChecker.Core.Commands; using Genius.PriceChecker.Core.Models; using Genius.PriceChecker.Core.Repositories; -namespace Genius.PriceChecker.Core.CommandHandlers +namespace Genius.PriceChecker.Core.CommandHandlers; + +internal sealed class ProductDropPricesCommandHandler : ICommandHandler { - internal sealed class ProductDropPricesCommandHandler : ICommandHandler - { - private readonly IProductRepository _productRepo; - private readonly IProductQueryService _productQuery; + private readonly IProductRepository _productRepo; + private readonly IProductQueryService _productQuery; - public ProductDropPricesCommandHandler(IProductRepository productRepo, IProductQueryService productQuery) - { - _productRepo = productRepo; - _productQuery = productQuery; - } + public ProductDropPricesCommandHandler(IProductRepository productRepo, IProductQueryService productQuery) + { + _productRepo = productRepo; + _productQuery = productQuery; + } - public Task ProcessAsync(ProductDropPricesCommand command) - { - var product = _productQuery.FindById(command.ProductId); + public Task ProcessAsync(ProductDropPricesCommand command) + { + var product = _productQuery.FindById(command.ProductId); + Guard.AgainstNull(product, nameof(product)); - product.Lowest = null; - product.Recent = Array.Empty(); + product.Lowest = null; + product.Recent = Array.Empty(); - _productRepo.Store(product); + _productRepo.Store(product); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/PriceChecker.Core/CommandHandlers/ProductEnqueueScanCommandHandler.cs b/PriceChecker.Core/CommandHandlers/ProductEnqueueScanCommandHandler.cs index ce65b68..3beab6f 100644 --- a/PriceChecker.Core/CommandHandlers/ProductEnqueueScanCommandHandler.cs +++ b/PriceChecker.Core/CommandHandlers/ProductEnqueueScanCommandHandler.cs @@ -3,22 +3,21 @@ using Genius.PriceChecker.Core.Commands; using Genius.PriceChecker.Core.Services; -namespace Genius.PriceChecker.Core.CommandHandlers +namespace Genius.PriceChecker.Core.CommandHandlers; + +internal sealed class ProductEnqueueScanCommandHandler : ICommandHandler { - internal sealed class ProductEnqueueScanCommandHandler : ICommandHandler - { - private readonly IProductPriceManager _productMng; + private readonly IProductPriceManager _productMng; - public ProductEnqueueScanCommandHandler(IProductPriceManager productMng) - { - _productMng = productMng; - } + public ProductEnqueueScanCommandHandler(IProductPriceManager productMng) + { + _productMng = productMng; + } - public Task ProcessAsync(ProductEnqueueScanCommand command) - { - _productMng.EnqueueScan(command.ProductId); + public Task ProcessAsync(ProductEnqueueScanCommand command) + { + _productMng.EnqueueScan(command.ProductId); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/PriceChecker.Core/Commands/AgentDeleteCommand.cs b/PriceChecker.Core/Commands/AgentDeleteCommand.cs index 6943199..ea18d1c 100644 --- a/PriceChecker.Core/Commands/AgentDeleteCommand.cs +++ b/PriceChecker.Core/Commands/AgentDeleteCommand.cs @@ -1,15 +1,13 @@ -using System; using Genius.Atom.Infrastructure.Commands; -namespace Genius.PriceChecker.Core.Commands +namespace Genius.PriceChecker.Core.Commands; + +public sealed class AgentDeleteCommand : ICommandMessage { - public sealed class AgentDeleteCommand : ICommandMessage + public AgentDeleteCommand(Guid agentId) { - public AgentDeleteCommand(Guid agentId) - { - AgentId = agentId; - } - - public Guid AgentId { get; } + AgentId = agentId; } + + public Guid AgentId { get; } } diff --git a/PriceChecker.Core/Commands/AgentsStoreWithOverwriteCommand.cs b/PriceChecker.Core/Commands/AgentsStoreWithOverwriteCommand.cs index c2065a4..fa69716 100644 --- a/PriceChecker.Core/Commands/AgentsStoreWithOverwriteCommand.cs +++ b/PriceChecker.Core/Commands/AgentsStoreWithOverwriteCommand.cs @@ -3,15 +3,14 @@ using Genius.Atom.Infrastructure.Commands; using Genius.PriceChecker.Core.Models; -namespace Genius.PriceChecker.Core.Commands +namespace Genius.PriceChecker.Core.Commands; + +public sealed class AgentsStoreWithOverwriteCommand : ICommandMessage { - public sealed class AgentsStoreWithOverwriteCommand : ICommandMessage + public AgentsStoreWithOverwriteCommand(IEnumerable agents) { - public AgentsStoreWithOverwriteCommand(IEnumerable agents) - { - Agents = agents.ToArray(); - } - - public Agent[] Agents { get; } + Agents = agents.ToArray(); } + + public Agent[] Agents { get; } } diff --git a/PriceChecker.Core/Commands/ProductCreateCommand.cs b/PriceChecker.Core/Commands/ProductCreateCommand.cs index 6bb86b2..837aec9 100644 --- a/PriceChecker.Core/Commands/ProductCreateCommand.cs +++ b/PriceChecker.Core/Commands/ProductCreateCommand.cs @@ -1,15 +1,13 @@ -using System; using Genius.Atom.Infrastructure.Commands; using Genius.PriceChecker.Core.Commands.UpdatableData; using Genius.PriceChecker.Core.Models; -namespace Genius.PriceChecker.Core.Commands +namespace Genius.PriceChecker.Core.Commands; + +public sealed class ProductCreateCommand : ProductUpdatableData, ICommandMessageExchange { - public sealed class ProductCreateCommand : ProductUpdatableData, ICommandMessageExchange + public ProductCreateCommand(string name, string? category, string? description, ProductSource[] sources) + : base(name, category, description, sources) { - public ProductCreateCommand(string name, string category, string description, ProductSource[] sources) - : base(name, category, description, sources) - { - } } } diff --git a/PriceChecker.Core/Commands/ProductDeleteCommand.cs b/PriceChecker.Core/Commands/ProductDeleteCommand.cs index 208eb35..3ec2882 100644 --- a/PriceChecker.Core/Commands/ProductDeleteCommand.cs +++ b/PriceChecker.Core/Commands/ProductDeleteCommand.cs @@ -1,15 +1,13 @@ -using System; using Genius.Atom.Infrastructure.Commands; -namespace Genius.PriceChecker.Core.Commands +namespace Genius.PriceChecker.Core.Commands; + +public sealed class ProductDeleteCommand : ICommandMessage { - public sealed class ProductDeleteCommand : ICommandMessage + public ProductDeleteCommand(Guid productId) { - public ProductDeleteCommand(Guid productId) - { - ProductId = productId; - } - - public Guid ProductId { get; } + ProductId = productId; } + + public Guid ProductId { get; } } diff --git a/PriceChecker.Core/Commands/ProductDropPricesCommand.cs b/PriceChecker.Core/Commands/ProductDropPricesCommand.cs index 02167c4..e00547a 100644 --- a/PriceChecker.Core/Commands/ProductDropPricesCommand.cs +++ b/PriceChecker.Core/Commands/ProductDropPricesCommand.cs @@ -1,15 +1,13 @@ -using System; using Genius.Atom.Infrastructure.Commands; -namespace Genius.PriceChecker.Core.Commands +namespace Genius.PriceChecker.Core.Commands; + +public sealed class ProductDropPricesCommand : ICommandMessage { - public sealed class ProductDropPricesCommand : ICommandMessage + public ProductDropPricesCommand(Guid productId) { - public ProductDropPricesCommand(Guid productId) - { - ProductId = productId; - } - - public Guid ProductId { get; } + ProductId = productId; } + + public Guid ProductId { get; } } diff --git a/PriceChecker.Core/Commands/ProductEnqueueScanCommand.cs b/PriceChecker.Core/Commands/ProductEnqueueScanCommand.cs index d3e1f97..a9318df 100644 --- a/PriceChecker.Core/Commands/ProductEnqueueScanCommand.cs +++ b/PriceChecker.Core/Commands/ProductEnqueueScanCommand.cs @@ -1,15 +1,13 @@ -using System; using Genius.Atom.Infrastructure.Commands; -namespace Genius.PriceChecker.Core.Commands +namespace Genius.PriceChecker.Core.Commands; + +public sealed class ProductEnqueueScanCommand : ICommandMessage { - public sealed class ProductEnqueueScanCommand : ICommandMessage + public ProductEnqueueScanCommand(Guid productId) { - public ProductEnqueueScanCommand(Guid productId) - { - ProductId = productId; - } - - public Guid ProductId { get; } + ProductId = productId; } + + public Guid ProductId { get; } } diff --git a/PriceChecker.Core/Commands/ProductUpdateCommand.cs b/PriceChecker.Core/Commands/ProductUpdateCommand.cs index a6562fb..59d08fb 100644 --- a/PriceChecker.Core/Commands/ProductUpdateCommand.cs +++ b/PriceChecker.Core/Commands/ProductUpdateCommand.cs @@ -1,18 +1,16 @@ -using System; using Genius.Atom.Infrastructure.Commands; using Genius.PriceChecker.Core.Commands.UpdatableData; using Genius.PriceChecker.Core.Models; -namespace Genius.PriceChecker.Core.Commands +namespace Genius.PriceChecker.Core.Commands; + +public sealed class ProductUpdateCommand : ProductUpdatableData, ICommandMessage { - public sealed class ProductUpdateCommand : ProductUpdatableData, ICommandMessage + public ProductUpdateCommand(Guid productId, string name, string? category, string? description, ProductSource[] sources) + : base(name, category, description, sources) { - public ProductUpdateCommand(Guid productId, string name, string category, string description, ProductSource[] sources) - : base(name, category, description, sources) - { - ProductId = productId; - } - - public Guid ProductId { get; } + ProductId = productId; } + + public Guid ProductId { get; } } diff --git a/PriceChecker.Core/Commands/UpdatableData/ProductUpdatableData.cs b/PriceChecker.Core/Commands/UpdatableData/ProductUpdatableData.cs index 27e7bec..e05bd34 100644 --- a/PriceChecker.Core/Commands/UpdatableData/ProductUpdatableData.cs +++ b/PriceChecker.Core/Commands/UpdatableData/ProductUpdatableData.cs @@ -1,20 +1,19 @@ using Genius.PriceChecker.Core.Models; -namespace Genius.PriceChecker.Core.Commands.UpdatableData +namespace Genius.PriceChecker.Core.Commands.UpdatableData; + +public abstract class ProductUpdatableData { - public abstract class ProductUpdatableData + protected ProductUpdatableData(string name, string? category, string? description, ProductSource[] sources) { - protected ProductUpdatableData(string name, string category, string description, ProductSource[] sources) - { - Name = name; - Category = category; - Description = description; - Sources = sources; - } - - public string Name { get; } - public string Category { get; } - public string Description { get; } - public ProductSource[] Sources { get; } + Name = name; + Category = category; + Description = description; + Sources = sources; } + + public string Name { get; } + public string? Category { get; } + public string? Description { get; } + public ProductSource[] Sources { get; } } diff --git a/PriceChecker.Core/Messages/AgentsAffectedEvent.cs b/PriceChecker.Core/Messages/AgentsAffectedEvent.cs index 3d4b035..7caabed 100644 --- a/PriceChecker.Core/Messages/AgentsAffectedEvent.cs +++ b/PriceChecker.Core/Messages/AgentsAffectedEvent.cs @@ -1,8 +1,7 @@ using Genius.Atom.Infrastructure.Events; -namespace Genius.PriceChecker.Core.Messages +namespace Genius.PriceChecker.Core.Messages; + +public sealed class AgentsAffectedEvent : IEventMessage { - public sealed class AgentsAffectedEvent : IEventMessage - { - } } diff --git a/PriceChecker.Core/Messages/PriceSeekResult.cs b/PriceChecker.Core/Messages/PriceSeekResult.cs index b934dd1..5dd48d4 100644 --- a/PriceChecker.Core/Messages/PriceSeekResult.cs +++ b/PriceChecker.Core/Messages/PriceSeekResult.cs @@ -1,11 +1,6 @@ -using System; +namespace Genius.PriceChecker.Core.Messages; -namespace Genius.PriceChecker.Core.Messages -{ - public sealed class PriceSeekResult - { - public Guid ProductSourceId { get; set; } - public string AgentKey { get; set; } - public decimal Price { get; set; } - } -} +public readonly record struct PriceSeekResult( + Guid ProductSourceId, + string AgentKey, + decimal Price); diff --git a/PriceChecker.Core/Messages/ProductAutoScanStartedEvent.cs b/PriceChecker.Core/Messages/ProductAutoScanStartedEvent.cs index dbdf4df..705dbf7 100644 --- a/PriceChecker.Core/Messages/ProductAutoScanStartedEvent.cs +++ b/PriceChecker.Core/Messages/ProductAutoScanStartedEvent.cs @@ -1,14 +1,13 @@ using Genius.Atom.Infrastructure.Events; -namespace Genius.PriceChecker.Core.Messages +namespace Genius.PriceChecker.Core.Messages; + +public sealed class ProductAutoScanStartedEvent : IEventMessage { - public sealed class ProductAutoScanStartedEvent : IEventMessage + public ProductAutoScanStartedEvent(int productsCount) { - public ProductAutoScanStartedEvent(int productsCount) - { - ProductsCount = productsCount; - } - - public int ProductsCount { get; } + ProductsCount = productsCount; } + + public int ProductsCount { get; } } diff --git a/PriceChecker.Core/Messages/ProductScanFailedEvent.cs b/PriceChecker.Core/Messages/ProductScanFailedEvent.cs index 82b084c..7d44633 100644 --- a/PriceChecker.Core/Messages/ProductScanFailedEvent.cs +++ b/PriceChecker.Core/Messages/ProductScanFailedEvent.cs @@ -1,17 +1,16 @@ using Genius.PriceChecker.Core.Models; using Genius.Atom.Infrastructure.Events; -namespace Genius.PriceChecker.Core.Messages +namespace Genius.PriceChecker.Core.Messages; + +public sealed class ProductScanFailedEvent : IEventMessage { - public sealed class ProductScanFailedEvent : IEventMessage + public ProductScanFailedEvent(Product product, string errorMessage) { - public ProductScanFailedEvent(Product product, string errorMessage) - { - Product = product; - ErrorMessage = errorMessage; - } - - public Product Product { get; } - public string ErrorMessage { get; } + Product = product; + ErrorMessage = errorMessage; } -} \ No newline at end of file + + public Product Product { get; } + public string ErrorMessage { get; } +} diff --git a/PriceChecker.Core/Messages/ProductScanStartedEvent.cs b/PriceChecker.Core/Messages/ProductScanStartedEvent.cs index 114bbfd..f9770f2 100644 --- a/PriceChecker.Core/Messages/ProductScanStartedEvent.cs +++ b/PriceChecker.Core/Messages/ProductScanStartedEvent.cs @@ -1,15 +1,14 @@ using Genius.PriceChecker.Core.Models; using Genius.Atom.Infrastructure.Events; -namespace Genius.PriceChecker.Core.Messages +namespace Genius.PriceChecker.Core.Messages; + +public sealed class ProductScanStartedEvent : IEventMessage { - public sealed class ProductScanStartedEvent : IEventMessage + public ProductScanStartedEvent(Product product) { - public ProductScanStartedEvent(Product product) - { - Product = product; - } - - public Product Product { get; } + Product = product; } + + public Product Product { get; } } diff --git a/PriceChecker.Core/Messages/ProductScannedEvent.cs b/PriceChecker.Core/Messages/ProductScannedEvent.cs index bd020c8..752c816 100644 --- a/PriceChecker.Core/Messages/ProductScannedEvent.cs +++ b/PriceChecker.Core/Messages/ProductScannedEvent.cs @@ -1,17 +1,16 @@ using Genius.PriceChecker.Core.Models; using Genius.Atom.Infrastructure.Events; -namespace Genius.PriceChecker.Core.Messages +namespace Genius.PriceChecker.Core.Messages; + +public sealed class ProductScannedEvent : IEventMessage { - public sealed class ProductScannedEvent : IEventMessage + public ProductScannedEvent(Product product, bool lowestPriceUpdated) { - public ProductScannedEvent(Product product, bool lowestPriceUpdated) - { - Product = product; - LowestPriceUpdated = lowestPriceUpdated; - } - - public Product Product { get; } - public bool LowestPriceUpdated { get; } + Product = product; + LowestPriceUpdated = lowestPriceUpdated; } + + public Product Product { get; } + public bool LowestPriceUpdated { get; } } diff --git a/PriceChecker.Core/Messages/ProductsAffectedEvent.cs b/PriceChecker.Core/Messages/ProductsAffectedEvent.cs index c361885..cd8b24d 100644 --- a/PriceChecker.Core/Messages/ProductsAffectedEvent.cs +++ b/PriceChecker.Core/Messages/ProductsAffectedEvent.cs @@ -1,8 +1,7 @@ using Genius.Atom.Infrastructure.Events; -namespace Genius.PriceChecker.Core.Messages +namespace Genius.PriceChecker.Core.Messages; + +public sealed class ProductsAffectedEvent : IEventMessage { - public sealed class ProductsAffectedEvent : IEventMessage - { - } } diff --git a/PriceChecker.Core/Messages/SettingsUpdatedEvent.cs b/PriceChecker.Core/Messages/SettingsUpdatedEvent.cs index 4f04150..5b0d81b 100644 --- a/PriceChecker.Core/Messages/SettingsUpdatedEvent.cs +++ b/PriceChecker.Core/Messages/SettingsUpdatedEvent.cs @@ -1,15 +1,14 @@ using Genius.PriceChecker.Core.Models; using Genius.Atom.Infrastructure.Events; -namespace Genius.PriceChecker.Core.Messages +namespace Genius.PriceChecker.Core.Messages; + +public sealed class SettingsUpdatedEvent : IEventMessage { - public sealed class SettingsUpdatedEvent : IEventMessage + public SettingsUpdatedEvent(Settings settings) { - public SettingsUpdatedEvent(Settings settings) - { - Settings = settings; - } - - public Settings Settings { get; } + Settings = settings; } + + public Settings Settings { get; } } diff --git a/PriceChecker.Core/Models/Agent.cs b/PriceChecker.Core/Models/Agent.cs index c5cac79..9717ca5 100644 --- a/PriceChecker.Core/Models/Agent.cs +++ b/PriceChecker.Core/Models/Agent.cs @@ -1,12 +1,11 @@ using Genius.Atom.Infrastructure.Entities; -namespace Genius.PriceChecker.Core.Models +namespace Genius.PriceChecker.Core.Models; + +public class Agent : EntityBase { - public class Agent : EntityBase - { - public string Key { get; set; } - public string Url { get; set; } - public string PricePattern { get; set; } - public char DecimalDelimiter { get; set; } - } + public string Key { get; set; } = null!; + public string Url { get; set; } = null!; + public string PricePattern { get; set; } = null!; + public char DecimalDelimiter { get; set; } } diff --git a/PriceChecker.Core/Models/Product.cs b/PriceChecker.Core/Models/Product.cs index a14c060..ac44024 100644 --- a/PriceChecker.Core/Models/Product.cs +++ b/PriceChecker.Core/Models/Product.cs @@ -1,22 +1,13 @@ -using System; -using System.Diagnostics.CodeAnalysis; using Genius.Atom.Infrastructure.Entities; -namespace Genius.PriceChecker.Core.Models +namespace Genius.PriceChecker.Core.Models; + +public class Product : EntityBase { - public class Product : EntityBase - { - [MaybeNull] - public string Category { get; set; } - [NotNull] - public string Name { get; set; } - [MaybeNull] - public string Description { get; set; } - [NotNull] - public ProductSource[] Sources { get; set; } = Array.Empty(); - [MaybeNull] - public ProductPrice Lowest { get; set; } - [NotNull] - public ProductPrice[] Recent { get; set; } = Array.Empty(); - } -} \ No newline at end of file + public string? Category { get; set; } + public string Name { get; set; } = null!; + public string? Description { get; set; } + public ProductSource[] Sources { get; set; } = Array.Empty(); + public ProductPrice? Lowest { get; set; } + public ProductPrice[] Recent { get; set; } = Array.Empty(); +} diff --git a/PriceChecker.Core/Models/ProductPrice.cs b/PriceChecker.Core/Models/ProductPrice.cs index 16c0f6d..a7b311d 100644 --- a/PriceChecker.Core/Models/ProductPrice.cs +++ b/PriceChecker.Core/Models/ProductPrice.cs @@ -1,19 +1,17 @@ -using System; using System.Text.Json.Serialization; -namespace Genius.PriceChecker.Core.Models +namespace Genius.PriceChecker.Core.Models; + +public class ProductPrice { - public class ProductPrice - { - public Guid ProductSourceId { get; set; } - [JsonIgnore] - public ProductSource ProductSource { get; set; } - public decimal Price { get; set; } - public DateTime FoundDate { get; set; } + public Guid ProductSourceId { get; set; } + [JsonIgnore] + public ProductSource ProductSource { get; set; } = null!; + public decimal Price { get; set; } + public DateTime FoundDate { get; set; } - public override string ToString() - { - return $"{ProductSource?.Product?.Name ?? ProductSourceId.ToString()} = {Price}"; - } + public override string ToString() + { + return $"{ProductSource?.Product?.Name ?? ProductSourceId.ToString()} = {Price}"; } } diff --git a/PriceChecker.Core/Models/ProductScanStatus.cs b/PriceChecker.Core/Models/ProductScanStatus.cs index 5ae4e15..968a268 100644 --- a/PriceChecker.Core/Models/ProductScanStatus.cs +++ b/PriceChecker.Core/Models/ProductScanStatus.cs @@ -1,13 +1,12 @@ -namespace Genius.PriceChecker.Core.Models +namespace Genius.PriceChecker.Core.Models; + +public enum ProductScanStatus { - public enum ProductScanStatus - { - NotScanned, - Scanning, - ScannedOk, - ScannedWithErrors, - ScannedNewLowest, - Outdated, - Failed - } + NotScanned, + Scanning, + ScannedOk, + ScannedWithErrors, + ScannedNewLowest, + Outdated, + Failed } diff --git a/PriceChecker.Core/Models/ProductSource.cs b/PriceChecker.Core/Models/ProductSource.cs index e8d89ad..3ebe34c 100644 --- a/PriceChecker.Core/Models/ProductSource.cs +++ b/PriceChecker.Core/Models/ProductSource.cs @@ -1,18 +1,16 @@ -using System; using System.Text.Json.Serialization; -namespace Genius.PriceChecker.Core.Models +namespace Genius.PriceChecker.Core.Models; + +public class ProductSource { - public class ProductSource - { - public Guid Id { get; set; } - public string AgentKey { get; set; } - public string AgentArgument { get; set; } + public Guid Id { get; set; } + public string AgentKey { get; set; } = null!; + public string AgentArgument { get; set; } = null!; - // Relations: - [JsonIgnore] - public Product Product { get; internal set; } - [JsonIgnore] - public Agent Agent { get; internal set; } - } -} \ No newline at end of file + // Relations: + [JsonIgnore] + public Product Product { get; internal set; } = null!; + [JsonIgnore] + public Agent Agent { get; internal set; } = null!; +} diff --git a/PriceChecker.Core/Models/Settings.cs b/PriceChecker.Core/Models/Settings.cs index 2055c0d..de400fb 100644 --- a/PriceChecker.Core/Models/Settings.cs +++ b/PriceChecker.Core/Models/Settings.cs @@ -1,8 +1,7 @@ -namespace Genius.PriceChecker.Core.Models +namespace Genius.PriceChecker.Core.Models; + +public class Settings { - public class Settings - { - public bool AutoRefreshEnabled { get; set; } - public int AutoRefreshMinutes { get; set; } - } + public bool AutoRefreshEnabled { get; set; } + public int AutoRefreshMinutes { get; set; } } diff --git a/PriceChecker.Core/Module.cs b/PriceChecker.Core/Module.cs index ee41c78..12265ce 100644 --- a/PriceChecker.Core/Module.cs +++ b/PriceChecker.Core/Module.cs @@ -1,4 +1,6 @@ -using System; +global using System; +global using Genius.Atom.Infrastructure; + using System.Diagnostics.CodeAnalysis; using Genius.Atom.Infrastructure.Commands; using Genius.PriceChecker.Core.CommandHandlers; @@ -7,42 +9,41 @@ using Genius.PriceChecker.Core.Services; using Microsoft.Extensions.DependencyInjection; -namespace Genius.PriceChecker.Core +namespace Genius.PriceChecker.Core; + +[ExcludeFromCodeCoverage] +public static class Module { - [ExcludeFromCodeCoverage] - public static class Module + public static void Configure(IServiceCollection services) { - public static void Configure(IServiceCollection services) - { - // Repositories - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetService()); - services.AddSingleton(sp => sp.GetService()); - services.AddSingleton(); + // Repositories + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetService()!); + services.AddSingleton(sp => sp.GetService()!); + services.AddSingleton(); - // Query services - services.AddSingleton(sp => sp.GetService()); - services.AddSingleton(sp => sp.GetService()); + // Query services + services.AddSingleton(sp => sp.GetService()!); + services.AddSingleton(sp => sp.GetService()!); - // Services - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); + // Services + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); - // Command Handlers - services.AddScoped, AgentDeleteCommandHandler>(); - services.AddScoped, AgentsStoreWithOverwriteCommandHandler>(); - services.AddScoped, ProductCreateOrUpdateCommandHandler>(); - services.AddScoped, ProductCreateOrUpdateCommandHandler>(); - services.AddScoped, ProductDeleteCommandHandler>(); - services.AddScoped, ProductDropPricesCommandHandler>(); - services.AddScoped, ProductEnqueueScanCommandHandler>(); - } + // Command Handlers + services.AddScoped, AgentDeleteCommandHandler>(); + services.AddScoped, AgentsStoreWithOverwriteCommandHandler>(); + services.AddScoped, ProductCreateOrUpdateCommandHandler>(); + services.AddScoped, ProductCreateOrUpdateCommandHandler>(); + services.AddScoped, ProductDeleteCommandHandler>(); + services.AddScoped, ProductDropPricesCommandHandler>(); + services.AddScoped, ProductEnqueueScanCommandHandler>(); + } - public static void Initialize(IServiceProvider serviceProvider) - { - serviceProvider.GetService().AutoRefreshInitialize(); - } + public static void Initialize(IServiceProvider serviceProvider) + { + serviceProvider.GetService()!.AutoRefreshInitialize(); } -} \ No newline at end of file +} diff --git a/PriceChecker.Core/PriceChecker.Core.csproj b/PriceChecker.Core/PriceChecker.Core.csproj index df266d9..1885442 100644 --- a/PriceChecker.Core/PriceChecker.Core.csproj +++ b/PriceChecker.Core/PriceChecker.Core.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 Genius.PriceChecker.Core @@ -22,7 +22,7 @@ - ..\..\atom\Atom.Infrastructure\bin\Debug\net5.0\Genius.Atom.Infrastructure.dll + ..\..\atom\Atom.Infrastructure\bin\Debug\net6.0\Genius.Atom.Infrastructure.dll diff --git a/PriceChecker.Core/Repositories/AgentRepository.cs b/PriceChecker.Core/Repositories/AgentRepository.cs index 564f9cd..dc8f2e2 100644 --- a/PriceChecker.Core/Repositories/AgentRepository.cs +++ b/PriceChecker.Core/Repositories/AgentRepository.cs @@ -5,27 +5,26 @@ using Genius.PriceChecker.Core.Models; using Microsoft.Extensions.Logging; -namespace Genius.PriceChecker.Core.Repositories +namespace Genius.PriceChecker.Core.Repositories; + +public interface IAgentQueryService : IEntityQueryService { - public interface IAgentQueryService : IEntityQueryService - { - Agent FindByKey(string agentKey); - } + Agent? FindByKey(string agentKey); +} - public interface IAgentRepository : IRepository +public interface IAgentRepository : IRepository +{ +} + +internal sealed class AgentRepository : RepositoryBase, IAgentRepository, IAgentQueryService +{ + public AgentRepository(IEventBus eventBus, IJsonPersister persister, ILogger logger) + : base(eventBus, persister, logger) { } - internal sealed class AgentRepository : RepositoryBase, IAgentRepository, IAgentQueryService + public Agent? FindByKey(string agentKey) { - public AgentRepository(IEventBus eventBus, IJsonPersister persister, ILogger logger) - : base(eventBus, persister, logger) - { - } - - public Agent FindByKey(string agentKey) - { - return GetAll().FirstOrDefault(x => x.Key == agentKey); - } + return GetAll().FirstOrDefault(x => x.Key == agentKey); } } diff --git a/PriceChecker.Core/Repositories/ProductRepository.cs b/PriceChecker.Core/Repositories/ProductRepository.cs index fa83e71..0cccb2b 100644 --- a/PriceChecker.Core/Repositories/ProductRepository.cs +++ b/PriceChecker.Core/Repositories/ProductRepository.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using Genius.Atom.Infrastructure.Entities; using Genius.Atom.Infrastructure.Events; @@ -6,41 +5,40 @@ using Genius.PriceChecker.Core.Models; using Microsoft.Extensions.Logging; -namespace Genius.PriceChecker.Core.Repositories +namespace Genius.PriceChecker.Core.Repositories; + +public interface IProductQueryService : IEntityQueryService { - public interface IProductQueryService : IEntityQueryService - { - } +} - public interface IProductRepository : IRepository +public interface IProductRepository : IRepository +{ +} + +internal sealed class ProductRepository : RepositoryBase, IProductRepository, IProductQueryService +{ + private readonly IAgentQueryService _agentRepo; + + public ProductRepository(IEventBus eventBus, IJsonPersister persister, + IAgentQueryService agentQuery, + ILogger logger) + : base(eventBus, persister, logger) { + _agentRepo = agentQuery; } - internal sealed class ProductRepository : RepositoryBase, IProductRepository, IProductQueryService + protected override void FillupRelations(Product product) { - private readonly IAgentQueryService _agentRepo; + var sourcesDict = product.Sources.ToDictionary(x => x.Id); - public ProductRepository(IEventBus eventBus, IJsonPersister persister, - IAgentQueryService agentQuery, - ILogger logger) - : base(eventBus, persister, logger) + foreach (var productSource in product.Sources) { - _agentRepo = agentQuery; + productSource.Product = product; + productSource.Agent = _agentRepo.FindByKey(productSource.AgentKey).NotNull(); } - - protected override void FillupRelations(Product product) + foreach (var productPrice in product.Recent) { - var sourcesDict = product.Sources.ToDictionary(x => x.Id); - - foreach (var productSource in product.Sources) - { - productSource.Product = product; - productSource.Agent = _agentRepo.FindByKey(productSource.AgentKey); - } - foreach (var productPrice in product.Recent) - { - productPrice.ProductSource = sourcesDict[productPrice.ProductSourceId]; - } + productPrice.ProductSource = sourcesDict[productPrice.ProductSourceId]; } } } diff --git a/PriceChecker.Core/Repositories/SettingsRepository.cs b/PriceChecker.Core/Repositories/SettingsRepository.cs index c3e1187..752a4e6 100644 --- a/PriceChecker.Core/Repositories/SettingsRepository.cs +++ b/PriceChecker.Core/Repositories/SettingsRepository.cs @@ -1,53 +1,51 @@ -using System; using Genius.Atom.Infrastructure.Events; using Genius.Atom.Infrastructure.Persistence; using Genius.PriceChecker.Core.Messages; using Genius.PriceChecker.Core.Models; using Microsoft.Extensions.Logging; -namespace Genius.PriceChecker.Core.Repositories -{ - public interface ISettingsRepository - { - Settings Get(); - void Store(Settings settings); - } +namespace Genius.PriceChecker.Core.Repositories; - internal sealed class SettingsRepository : ISettingsRepository - { - private readonly IEventBus _eventBus; - private readonly IJsonPersister _persister; - private readonly ILogger _logger; +public interface ISettingsRepository +{ + Settings Get(); + void Store(Settings settings); +} - private const string FILENAME = @".\settings.json"; - private Settings _settings = null; +internal sealed class SettingsRepository : ISettingsRepository +{ + private readonly IEventBus _eventBus; + private readonly IJsonPersister _persister; + private readonly ILogger _logger; - public SettingsRepository(IEventBus eventBus, IJsonPersister persister, ILogger logger) - { - _persister = persister; - _logger = logger; - _eventBus = eventBus; + private const string FILENAME = @".\settings.json"; + private Settings _settings; - _settings = _persister.Load(FILENAME) ?? CreateDefaultSettings(); - } + public SettingsRepository(IEventBus eventBus, IJsonPersister persister, ILogger logger) + { + _persister = persister; + _logger = logger; + _eventBus = eventBus; - public Settings Get() => _settings; + _settings = _persister.Load(FILENAME) ?? CreateDefaultSettings(); + } - public void Store(Settings settings) - { - _settings = settings ?? throw new ArgumentNullException("Settings cannot be null."); + public Settings Get() => _settings; - _persister.Store(FILENAME, settings); + public void Store(Settings settings) + { + _settings = settings.NotNull(nameof(settings)); - _eventBus.Publish(new SettingsUpdatedEvent(settings)); + _persister.Store(FILENAME, settings); - _logger.LogInformation("Settings updated."); - } + _eventBus.Publish(new SettingsUpdatedEvent(settings)); - private Settings CreateDefaultSettings() - => new Settings { - AutoRefreshEnabled = false, - AutoRefreshMinutes = 1440 // 24 hours - }; + _logger.LogInformation("Settings updated."); } + + private static Settings CreateDefaultSettings() + => new() { + AutoRefreshEnabled = false, + AutoRefreshMinutes = 1440 // 24 hours + }; } diff --git a/PriceChecker.Core/Services/PriceSeeker.cs b/PriceChecker.Core/Services/PriceSeeker.cs index d204457..5a88f69 100644 --- a/PriceChecker.Core/Services/PriceSeeker.cs +++ b/PriceChecker.Core/Services/PriceSeeker.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Text.RegularExpressions; using System.Threading; @@ -9,100 +8,100 @@ using Genius.PriceChecker.Core.Models; using Microsoft.Extensions.Logging; -namespace Genius.PriceChecker.Core.Services +namespace Genius.PriceChecker.Core.Services; + +public interface IPriceSeeker +{ + Task SeekAsync(Product product, CancellationToken cancel); +} + +internal sealed class PriceSeeker : IPriceSeeker { - public interface IPriceSeeker + private readonly ITrickyHttpClient _trickyHttpClient; + private readonly IFileService _io; + private readonly ILogger _logger; + + private const char DEFAULT_DECIMAL_DELIMITER = '.'; + + private static readonly object _locker = new(); + + public PriceSeeker(ITrickyHttpClient trickyHttpClient, IFileService io, ILogger logger) { - Task SeekAsync(Product product, CancellationToken cancel); + _trickyHttpClient = trickyHttpClient; + _io = io; + _logger = logger; } - internal sealed class PriceSeeker : IPriceSeeker + public async Task SeekAsync(Product product, CancellationToken cancel) { - private readonly ITrickyHttpClient _trickyHttpClient; - private readonly IFileService _io; - private readonly ILogger _logger; - - private const char DEFAULT_DECIMAL_DELIMITER = '.'; - - private static readonly object _locker = new(); + var result = product.Sources.AsParallel().Select(async (productSource) => + await Seek(productSource, cancel)); + + return await Task.WhenAll(result) + .ContinueWith(t => t.Result? + .Where(x => x != null) + .Select(x => x!.Value) + .ToArray() ?? new PriceSeekResult[0], + TaskContinuationOptions.OnlyOnRanToCompletion); + } - public PriceSeeker(ITrickyHttpClient trickyHttpClient, IFileService io, ILogger logger) + private async Task Seek(ProductSource productSource, CancellationToken cancel) + { + var agent = productSource.Agent; + var url = string.Format(agent.Url, productSource.AgentArgument); + string? content; + try { - _trickyHttpClient = trickyHttpClient; - _io = io; - _logger = logger; + content = await _trickyHttpClient.DownloadContent(url, cancel); } - - public async Task SeekAsync(Product product, CancellationToken cancel) + catch (Exception ex) { - var result = product.Sources.AsParallel().Select(async (productSource) => - await Seek(productSource, cancel)); - - return await Task.WhenAll(result) - .ContinueWith(x => x.Result?.Where(x => x != null).ToArray() ?? new PriceSeekResult[0], TaskContinuationOptions.OnlyOnRanToCompletion); + _logger.LogError(ex, "Failed loading content for source `{productSourceAgentKey}`, url = `{url}`", productSource.AgentKey, url); + throw; } + if (content is null) + return null; - private async Task Seek(ProductSource productSource, CancellationToken cancel) + var re = new Regex(agent.PricePattern); + var match = re.Match(content); + if (!match.Success) { - var agent = productSource.Agent; - var url = string.Format(agent.Url, productSource.AgentArgument); - string content; - try - { - content = await _trickyHttpClient.DownloadContent(url, cancel); - } - catch (Exception ex) + var dumpFileName = $"dump ({productSource.Id}).log"; + lock(_locker) { - _logger.LogError(ex, $"Failed loading content for source `{productSource.AgentKey}`, url = `{url}`"); - throw; + _io.WriteTextToFile(dumpFileName, content); } - if (content == null) - return null; + _logger.LogError("Cannot match price from the given content. File = '{dumpFileName}', Url = '{url}'", dumpFileName, url); + return null; + } - var re = new Regex(agent.PricePattern); - var match = re.Match(content); - if (!match.Success) - { - var dumpFileName = $"dump ({productSource.Id}).log"; - lock(_locker) - { - _io.WriteTextToFile(dumpFileName, content); - } - _logger.LogError($"Cannot match price from the given content. File = '{dumpFileName}', Url = '{url}'"); - return null; - } + if (!TryParsePrice(match, out var price)) + return null; - if (!TryParsePrice(match, out var price)) - return null; + if (price <= 0.0m) + { + _logger.LogWarning("Price for product '{productSourceAgentArgument}' at '{agentId}' is invalid: {price}", + productSource.AgentArgument, agent.Id, price); + return null; + } - if (price <= 0.0m) - { - _logger.LogWarning($"Price for product '{productSource.AgentArgument}' at '{agent.Id}' is invalid: {price}"); - return null; - } + return new PriceSeekResult(productSource.Id, agent.Key, price); - return new PriceSeekResult { - ProductSourceId = productSource.Id, - AgentKey = agent.Key, - Price = price - }; + bool TryParsePrice(Match match, out decimal price) + { + var priceString = match.Groups["price"].Value; + if (agent.DecimalDelimiter != DEFAULT_DECIMAL_DELIMITER) + priceString = priceString.Replace(agent.DecimalDelimiter, DEFAULT_DECIMAL_DELIMITER); - bool TryParsePrice(Match match, out decimal price) + var priceConverted = decimal.TryParse(priceString, out price); + if (!priceConverted) { - var priceString = match.Groups["price"].Value; - if (agent.DecimalDelimiter != DEFAULT_DECIMAL_DELIMITER) - priceString = priceString.Replace(agent.DecimalDelimiter, DEFAULT_DECIMAL_DELIMITER); - - var priceConverted = decimal.TryParse(priceString, out price); - if (!priceConverted) - { - _logger.LogError($"Could not convert the price '{priceString}' to decimal. Url = '{url}'"); - return false; - } - - return true; + _logger.LogError("Could not convert the price '{priceString}' to decimal. Url = '{url}'", priceString, url); + return false; } + + return true; } } } diff --git a/PriceChecker.Core/Services/ProductPriceManager.cs b/PriceChecker.Core/Services/ProductPriceManager.cs index 067478f..3d88f8c 100644 --- a/PriceChecker.Core/Services/ProductPriceManager.cs +++ b/PriceChecker.Core/Services/ProductPriceManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Concurrency; @@ -10,175 +9,176 @@ using Genius.Atom.Infrastructure.Events; using Microsoft.Extensions.Logging; -namespace Genius.PriceChecker.Core.Services +namespace Genius.PriceChecker.Core.Services; + +public interface IProductPriceManager : IDisposable +{ + void EnqueueScan(Guid productId); + void AutoRefreshInitialize(); +} + +internal sealed class ProductPriceManager : IProductPriceManager { - public interface IProductPriceManager : IDisposable + private readonly IProductRepository _productRepo; + private readonly IProductQueryService _productQuery; + private readonly IPriceSeeker _priceSeeker; + private readonly IEventBus _eventBus; + private readonly ISettingsRepository _settingsRepo; + private readonly ILogger _logger; + + private IDisposable? _scheduledAutoRefresh; + private int _previousAutoRefreshMinutes; + + private readonly TimeSpan RecentPeriod = TimeSpan.FromHours(3); + + public ProductPriceManager(IProductRepository productRepo, + IProductQueryService productQuery, + IPriceSeeker priceSeeker, IEventBus eventBus, + ISettingsRepository settingsRepo, + ILogger logger) { - void EnqueueScan(Guid productId); - void AutoRefreshInitialize(); + _productRepo = productRepo; + _productQuery = productQuery; + _priceSeeker = priceSeeker; + _eventBus = eventBus; + _settingsRepo = settingsRepo; + _logger = logger; + + eventBus.WhenFired().Subscribe(args => { + if (args.Settings.AutoRefreshEnabled && _scheduledAutoRefresh is null + || args.Settings.AutoRefreshMinutes != _previousAutoRefreshMinutes) + { + _scheduledAutoRefresh?.Dispose(); + AutoRefreshInitialize(); + } + }); } - internal sealed class ProductPriceManager : IProductPriceManager + public void EnqueueScan(Guid productId) { - private readonly IProductRepository _productRepo; - private readonly IProductQueryService _productQuery; - private readonly IPriceSeeker _priceSeeker; - private readonly IEventBus _eventBus; - private readonly ISettingsRepository _settingsRepo; - private readonly ILogger _logger; - - private IDisposable _scheduledAutoRefresh; - private int _previousAutoRefreshMinutes; - - private readonly TimeSpan RecentPeriod = TimeSpan.FromHours(3); - - public ProductPriceManager(IProductRepository productRepo, - IProductQueryService productQuery, - IPriceSeeker priceSeeker, IEventBus eventBus, - ISettingsRepository settingsRepo, - ILogger logger) + var product = _productQuery.FindById(productId); + if (product == null) { - _productRepo = productRepo; - _productQuery = productQuery; - _priceSeeker = priceSeeker; - _eventBus = eventBus; - _settingsRepo = settingsRepo; - _logger = logger; - - eventBus.WhenFired().Subscribe(args => { - if (args.Settings.AutoRefreshEnabled && _scheduledAutoRefresh == null - || args.Settings.AutoRefreshMinutes != _previousAutoRefreshMinutes) - { - _scheduledAutoRefresh?.Dispose(); - AutoRefreshInitialize(); - } - }); + _logger.LogError("Product with ID '{productId}' was not found.", productId); + return; } - public void EnqueueScan(Guid productId) - { - var product = _productQuery.FindById(productId); - if (product == null) - { - _logger.LogError($"Product with ID '{productId}' was not found."); - return; - } + EnqueueScan(product, ignoreRecentDate: true, CancellationToken.None); + } - EnqueueScan(product, ignoreRecentDate: true, CancellationToken.None); - } + public void Dispose() + { + _scheduledAutoRefresh?.Dispose(); + } - public void Dispose() - { - _scheduledAutoRefresh?.Dispose(); - } + public void AutoRefreshInitialize() + { + _scheduledAutoRefresh?.Dispose(); + _scheduledAutoRefresh = null; - public void AutoRefreshInitialize() + if (!_settingsRepo.Get().AutoRefreshEnabled) { - _scheduledAutoRefresh?.Dispose(); - _scheduledAutoRefresh = null; - - if (!_settingsRepo.Get().AutoRefreshEnabled) - { - return; - } - - _previousAutoRefreshMinutes = _settingsRepo.Get().AutoRefreshMinutes; - - _scheduledAutoRefresh = TaskPoolScheduler.Default.ScheduleAsync( - TimeSpan.FromMinutes(_previousAutoRefreshMinutes), - async (scheduler, cancel) => { - _logger.LogInformation("AutoRefresh worker started."); - List tasks = new(); - if (_settingsRepo.Get().AutoRefreshEnabled) - { - var products = _productQuery.GetAll().ToList(); - _eventBus.Publish(new ProductAutoScanStartedEvent(products.Count)); - foreach (var product in products) - tasks.Add(EnqueueScan(product, ignoreRecentDate: false, cancel)); - } - - await Task.WhenAll(tasks); - - AutoRefreshInitialize(); - }); + return; } - private Task EnqueueScan(Product product, bool ignoreRecentDate, CancellationToken cancel) - { - return Task.Run(async() => - { - try - { - await ScanForPricesAsync(product, ignoreRecentDate, cancel); - } - catch (Exception ex) + _previousAutoRefreshMinutes = _settingsRepo.Get().AutoRefreshMinutes; + + _scheduledAutoRefresh = TaskPoolScheduler.Default.ScheduleAsync( + TimeSpan.FromMinutes(_previousAutoRefreshMinutes), + async (scheduler, cancel) => { + _logger.LogInformation("AutoRefresh worker started."); + List tasks = new(); + if (_settingsRepo.Get().AutoRefreshEnabled) { - _eventBus.Publish(new ProductScanFailedEvent(product, ex.Message)); - throw; + var products = _productQuery.GetAll().ToList(); + _eventBus.Publish(new ProductAutoScanStartedEvent(products.Count)); + foreach (var product in products) + tasks.Add(EnqueueScan(product, ignoreRecentDate: false, cancel)); } - }, cancel); - } - private async Task ScanForPricesAsync(Product product, bool ignoreRecentDate, CancellationToken cancel) + await Task.WhenAll(tasks); + + AutoRefreshInitialize(); + }); + } + + private Task EnqueueScan(Product product, bool ignoreRecentDate, CancellationToken cancel) + { + return Task.Run(async() => { - if (!ignoreRecentDate && IsTooRecent(product)) + try { - _logger.LogTrace($"Price scanning '{product.Name}' cancelled due to recent results"); - //_eventBus.Publish(new ProductScanFailedEvent(product, "Scan cancelled due to recent results")); - _eventBus.Publish(new ProductScannedEvent(product, false)); - return; + await ScanForPricesAsync(product, ignoreRecentDate, cancel); } - - _eventBus.Publish(new ProductScanStartedEvent(product)); - - _logger.LogTrace($"Processing '{product.Name}'"); - var results = await _priceSeeker.SeekAsync(product, cancel); - if (!results.Any()) + catch (Exception ex) { - _logger.LogWarning($"Price scanning for '{product.Name}' failed or no results retrieved"); - _eventBus.Publish(new ProductScanFailedEvent(product, "Scan failed or no results retrieved")); - return; + _eventBus.Publish(new ProductScanFailedEvent(product, ex.Message)); + throw; } + }, cancel); + } - product.Recent = LogAndConvert(product, results); + private async Task ScanForPricesAsync(Product product, bool ignoreRecentDate, CancellationToken cancel) + { + if (!ignoreRecentDate && IsTooRecent(product)) + { + _logger.LogTrace("Price scanning '{productName}' cancelled due to recent results", product.Name); + //_eventBus.Publish(new ProductScanFailedEvent(product, "Scan cancelled due to recent results")); + _eventBus.Publish(new ProductScannedEvent(product, false)); + return; + } - var lowestPriceUpdated = false; - var minPrice = product.Recent.Min(x => x.Price); - if (product.Lowest == null || product.Lowest.Price >= minPrice) - { - if (product.Lowest != null && product.Lowest.Price > minPrice) - { - lowestPriceUpdated = true; - } - product.Lowest = product.Recent.First(x => x.Price == minPrice); + _eventBus.Publish(new ProductScanStartedEvent(product)); - _productRepo.Store(product); - } - - _eventBus.Publish(new ProductScannedEvent(product, lowestPriceUpdated)); + _logger.LogTrace("Processing '{productName}'", product.Name); + var results = await _priceSeeker.SeekAsync(product, cancel); + if (!results.Any()) + { + _logger.LogWarning("Price scanning for '{productName}' failed or no results retrieved", product.Name); + _eventBus.Publish(new ProductScanFailedEvent(product, "Scan failed or no results retrieved")); + return; } - private bool IsTooRecent(Product product) + product.Recent = LogAndConvert(product, results); + + var lowestPriceUpdated = false; + var minPrice = product.Recent.Min(x => x.Price); + if (product.Lowest == null || product.Lowest.Price >= minPrice) { - if (product.Recent.Length == 0) - return false; + if (product.Lowest != null && product.Lowest.Price > minPrice) + { + lowestPriceUpdated = true; + } + product.Lowest = product.Recent.First(x => x.Price == minPrice); - var recentDate = product.Recent.Max(x => x.FoundDate); - return DateTime.Now - recentDate < RecentPeriod; + _productRepo.Store(product); } - private ProductPrice[] LogAndConvert(Product product, IEnumerable results) + _eventBus.Publish(new ProductScannedEvent(product, lowestPriceUpdated)); + } + + private bool IsTooRecent(Product product) + { + if (product.Recent.Length == 0) + return false; + + var recentDate = product.Recent.Max(x => x.FoundDate); + return DateTime.Now - recentDate < RecentPeriod; + } + + private ProductPrice[] LogAndConvert(Product product, IEnumerable results) + { + var converted = results.Select(x => new ProductPrice { - var converted = results.Select(x => new ProductPrice - { - ProductSourceId = x.ProductSourceId, - Price = x.Price, - FoundDate = DateTime.Now - }).ToArray(); + ProductSourceId = x.ProductSourceId, + Price = x.Price, + FoundDate = DateTime.Now + }).ToArray(); - _logger.LogTrace($"Results retrieved for '{product.Name}': {string.Join(", ", converted.ToList())}"); + _logger.LogTrace("Results retrieved for '{productName}': {results}", + product.Name, + string.Join(", ", converted.ToList())); - return converted; - } + return converted; } } diff --git a/PriceChecker.Core/Services/ProductPriceTaskScheduler.cs b/PriceChecker.Core/Services/ProductPriceTaskScheduler.cs index 420ea8c..c10bd5b 100644 --- a/PriceChecker.Core/Services/ProductPriceTaskScheduler.cs +++ b/PriceChecker.Core/Services/ProductPriceTaskScheduler.cs @@ -4,59 +4,59 @@ using System.Threading; using System.Threading.Tasks; -namespace Genius.PriceChecker.Core.Services +namespace Genius.PriceChecker.Core.Services; + +internal sealed class ProductPriceTaskScheduler : TaskScheduler, IDisposable { - internal sealed class ProductPriceTaskScheduler : TaskScheduler, IDisposable - { - private BlockingCollection _tasksCollection = new(); - private readonly Thread _mainThread = null; + private readonly BlockingCollection _tasksCollection = new(); + private readonly Thread _mainThread; - public ProductPriceTaskScheduler() + public ProductPriceTaskScheduler() + { + _mainThread = new Thread(new ThreadStart(Execute)); + if (!_mainThread.IsAlive) { - _mainThread = new Thread(new ThreadStart(Execute)); - if (!_mainThread.IsAlive) - { - _mainThread.Start(); - } + _mainThread.Start(); } + } - public void Dispose() - { - _tasksCollection.CompleteAdding(); - _tasksCollection.Dispose(); - } + public void Dispose() + { + _mainThread.Interrupt(); + _tasksCollection.CompleteAdding(); + _tasksCollection.Dispose(); + } - protected override IEnumerable GetScheduledTasks() - { - return _tasksCollection.ToArray(); - } + protected override IEnumerable GetScheduledTasks() + { + return _tasksCollection.ToArray(); + } - protected override void QueueTask(Task task) + protected override void QueueTask(Task task) + { + if (task != null) { - if (task != null) - { - _tasksCollection.Add(task); - } + _tasksCollection.Add(task); } + } - protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) - { - return false; - } + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + return false; + } - private void Execute() + private void Execute() + { + try { - try - { - foreach (var task in _tasksCollection.GetConsumingEnumerable()) - { - TryExecuteTask(task); - } - } - catch (ObjectDisposedException) + foreach (var task in _tasksCollection.GetConsumingEnumerable()) { - // Just break + TryExecuteTask(task); } } + catch (ObjectDisposedException) + { + // Just break + } } -} \ No newline at end of file +} diff --git a/PriceChecker.Core/Services/ProductStatusProvider.cs b/PriceChecker.Core/Services/ProductStatusProvider.cs index e237f6c..8f3e2e2 100644 --- a/PriceChecker.Core/Services/ProductStatusProvider.cs +++ b/PriceChecker.Core/Services/ProductStatusProvider.cs @@ -1,30 +1,28 @@ -using System; using System.Linq; using Genius.PriceChecker.Core.Models; -namespace Genius.PriceChecker.Core.Services +namespace Genius.PriceChecker.Core.Services; + +public interface IProductStatusProvider { - public interface IProductStatusProvider - { - ProductScanStatus DetermineStatus(Product product); - } + ProductScanStatus DetermineStatus(Product product); +} - internal sealed class ProductStatusProvider : IProductStatusProvider - { - private readonly TimeSpan OutdatedPeriod = TimeSpan.FromHours(20); +internal sealed class ProductStatusProvider : IProductStatusProvider +{ + private readonly TimeSpan OutdatedPeriod = TimeSpan.FromHours(20); - public ProductScanStatus DetermineStatus(Product product) - { - if (product.Recent.Length == 0) - return ProductScanStatus.NotScanned; + public ProductScanStatus DetermineStatus(Product product) + { + if (product.Recent.Length == 0) + return ProductScanStatus.NotScanned; - if (DateTime.Now - product.Recent.Max(x => x.FoundDate) > OutdatedPeriod) - return ProductScanStatus.Outdated; + if (DateTime.Now - product.Recent.Max(x => x.FoundDate) > OutdatedPeriod) + return ProductScanStatus.Outdated; - if (product.Sources.Length != product.Recent.Length) - return ProductScanStatus.ScannedWithErrors; + if (product.Sources.Length != product.Recent.Length) + return ProductScanStatus.ScannedWithErrors; - return ProductScanStatus.ScannedOk; - } + return ProductScanStatus.ScannedOk; } } diff --git a/PriceChecker.UI.Tests/Helpers/TrackerScanContextTests.cs b/PriceChecker.UI.Tests/Helpers/TrackerScanContextTests.cs index 1d246b3..a52ff66 100644 --- a/PriceChecker.UI.Tests/Helpers/TrackerScanContextTests.cs +++ b/PriceChecker.UI.Tests/Helpers/TrackerScanContextTests.cs @@ -7,131 +7,130 @@ using Genius.PriceChecker.UI.Helpers; using Xunit; -namespace Genius.PriceChecker.UI.Tests.Helpers +namespace Genius.PriceChecker.UI.Tests.Helpers; + +public class TrackerScanContextTests : TestBase { - public class TrackerScanContextTests : TestBase + private readonly TrackerScanContext _sut; + + // Session values: + private readonly Subject _productAutoScanStartedEventSubject; + private TrackerScanStatus? _lastStatus = null; + private double? _lastProgress = null; + + public TrackerScanContextTests() + { + _productAutoScanStartedEventSubject = CreateEventSubject(); + + _sut = new TrackerScanContext(EventBusMock.Object); + + _sut.ScanProgress.Subscribe(x => { + _lastStatus = x.Status; + _lastProgress = x.Progress; + }); + } + + [Fact] + public void NotifyStarted__Resets_state_and_calculates_initial_progress() + { + // Arrange + const int count = 10; + + // Act + _sut.NotifyStarted(count); + + // Verify + const double expectedProgress = 1d / (count * 2); + Assert.True(_sut.IsStarted); + Assert.False(_sut.HasErrors); + Assert.False(_sut.HasNewLowestPrice); + Assert.Equal(0, _sut.FinishedJobs); + Assert.Equal(TrackerScanStatus.InProgress, _lastStatus); + Assert.Equal(expectedProgress, _lastProgress); + } + + [Fact] + public void NotifyProgressChange__When_ScannedOk__Increases_progress() + { + // Arrange + const int count = 2; + _sut.NotifyStarted(count); + + // Act + _sut.NotifyProgressChange(ProductScanStatus.ScannedOk); + + // Verify + const double expectedProgress = 0.5d; // 50% of 2 jobs + Assert.True(_sut.IsStarted); + Assert.False(_sut.HasErrors); + Assert.False(_sut.HasNewLowestPrice); + Assert.Equal(1, _sut.FinishedJobs); + Assert.Equal(TrackerScanStatus.InProgress, _lastStatus); + Assert.Equal(expectedProgress, _lastProgress); + } + + [Fact] + public void NotifyProgressChange__When_scanned_last_job__Finishes_progress() + { + // Arrange + _sut.NotifyStarted(2); + + // Act + _sut.NotifyProgressChange(ProductScanStatus.ScannedOk); + _sut.NotifyProgressChange(ProductScanStatus.ScannedOk); + + // Verify + Assert.False(_sut.IsStarted); + Assert.False(_sut.HasErrors); + Assert.False(_sut.HasNewLowestPrice); + Assert.Equal(2, _sut.FinishedJobs); + Assert.Equal(TrackerScanStatus.Finished, _lastStatus); + Assert.Equal(1, _lastProgress); + } + + [Fact] + public void NotifyProgressChange__When_ScannedWithErrors__Reports_about_errors() + { + // Arrange + _sut.NotifyStarted(Fixture.Create()); + + // Act + _sut.NotifyProgressChange(ProductScanStatus.ScannedWithErrors); + + // Verify + Assert.True(_sut.HasErrors); + Assert.Equal(TrackerScanStatus.InProgressWithErrors, _lastStatus); + } + + [Fact] + public void NotifyProgressChange__When_ScannedNewLowest__Reports_about_new_lowest() + { + // Arrange + _sut.NotifyStarted(Fixture.Create()); + + // Act + _sut.NotifyProgressChange(ProductScanStatus.ScannedNewLowest); + + // Verify + Assert.True(_sut.HasNewLowestPrice); + } + + [Fact] + public void ProductAutoScanStartedEvent_fired__Calls_NotifyStarted() { - private readonly TrackerScanContext _sut; - - // Session values: - private readonly Subject _productAutoScanStartedEventSubject; - private TrackerScanStatus? _lastStatus = null; - private double? _lastProgress = null; - - public TrackerScanContextTests() - { - _productAutoScanStartedEventSubject = CreateEventSubject(); - - _sut = new TrackerScanContext(EventBusMock.Object); - - _sut.ScanProgress.Subscribe(x => { - _lastStatus = x.Status; - _lastProgress = x.Progress; - }); - } - - [Fact] - public void NotifyStarted__Resets_state_and_calculates_initial_progress() - { - // Arrange - const int count = 10; - - // Act - _sut.NotifyStarted(count); - - // Verify - const double expectedProgress = 1d / (count * 2); - Assert.True(_sut.IsStarted); - Assert.False(_sut.HasErrors); - Assert.False(_sut.HasNewLowestPrice); - Assert.Equal(0, _sut.FinishedJobs); - Assert.Equal(TrackerScanStatus.InProgress, _lastStatus); - Assert.Equal(expectedProgress, _lastProgress); - } - - [Fact] - public void NotifyProgressChange__When_ScannedOk__Increases_progress() - { - // Arrange - const int count = 2; - _sut.NotifyStarted(count); - - // Act - _sut.NotifyProgressChange(ProductScanStatus.ScannedOk); - - // Verify - const double expectedProgress = 0.5d; // 50% of 2 jobs - Assert.True(_sut.IsStarted); - Assert.False(_sut.HasErrors); - Assert.False(_sut.HasNewLowestPrice); - Assert.Equal(1, _sut.FinishedJobs); - Assert.Equal(TrackerScanStatus.InProgress, _lastStatus); - Assert.Equal(expectedProgress, _lastProgress); - } - - [Fact] - public void NotifyProgressChange__When_scanned_last_job__Finishes_progress() - { - // Arrange - _sut.NotifyStarted(2); - - // Act - _sut.NotifyProgressChange(ProductScanStatus.ScannedOk); - _sut.NotifyProgressChange(ProductScanStatus.ScannedOk); - - // Verify - Assert.False(_sut.IsStarted); - Assert.False(_sut.HasErrors); - Assert.False(_sut.HasNewLowestPrice); - Assert.Equal(2, _sut.FinishedJobs); - Assert.Equal(TrackerScanStatus.Finished, _lastStatus); - Assert.Equal(1, _lastProgress); - } - - [Fact] - public void NotifyProgressChange__When_ScannedWithErrors__Reports_about_errors() - { - // Arrange - _sut.NotifyStarted(Fixture.Create()); - - // Act - _sut.NotifyProgressChange(ProductScanStatus.ScannedWithErrors); - - // Verify - Assert.True(_sut.HasErrors); - Assert.Equal(TrackerScanStatus.InProgressWithErrors, _lastStatus); - } - - [Fact] - public void NotifyProgressChange__When_ScannedNewLowest__Reports_about_new_lowest() - { - // Arrange - _sut.NotifyStarted(Fixture.Create()); - - // Act - _sut.NotifyProgressChange(ProductScanStatus.ScannedNewLowest); - - // Verify - Assert.True(_sut.HasNewLowestPrice); - } - - [Fact] - public void ProductAutoScanStartedEvent_fired__Calls_NotifyStarted() - { - // Arrange - const int count = 10; - - // Act - _productAutoScanStartedEventSubject.OnNext(new ProductAutoScanStartedEvent(count)); - - // Verify - const double expectedProgress = 1d / (count * 2); - Assert.True(_sut.IsStarted); - Assert.False(_sut.HasErrors); - Assert.False(_sut.HasNewLowestPrice); - Assert.Equal(0, _sut.FinishedJobs); - Assert.Equal(TrackerScanStatus.InProgress, _lastStatus); - Assert.Equal(expectedProgress, _lastProgress); - } + // Arrange + const int count = 10; + + // Act + _productAutoScanStartedEventSubject.OnNext(new ProductAutoScanStartedEvent(count)); + + // Verify + const double expectedProgress = 1d / (count * 2); + Assert.True(_sut.IsStarted); + Assert.False(_sut.HasErrors); + Assert.False(_sut.HasNewLowestPrice); + Assert.Equal(0, _sut.FinishedJobs); + Assert.Equal(TrackerScanStatus.InProgress, _lastStatus); + Assert.Equal(expectedProgress, _lastProgress); } } diff --git a/PriceChecker.UI.Tests/PriceChecker.UI.Tests.csproj b/PriceChecker.UI.Tests/PriceChecker.UI.Tests.csproj index 78c3383..e4d4c87 100644 --- a/PriceChecker.UI.Tests/PriceChecker.UI.Tests.csproj +++ b/PriceChecker.UI.Tests/PriceChecker.UI.Tests.csproj @@ -1,7 +1,7 @@ - net5.0-windows + net6.0-windows Genius.PriceChecker.UI.Tests false @@ -12,21 +12,21 @@ - ..\..\atom\Atom.Infrastructure\bin\Debug\net5.0\Genius.Atom.Infrastructure.dll + ..\..\atom\Atom.Infrastructure\bin\Debug\net6.0\Genius.Atom.Infrastructure.dll - ..\..\atom\Atom.UI.Forms\bin\Debug\net5.0-windows\Genius.Atom.UI.Forms.dll + ..\..\atom\Atom.UI.Forms\bin\Debug\net6.0-windows\Genius.Atom.UI.Forms.dll - ..\..\atom\Atom.UI.Forms.TestingUtil\bin\Debug\net5.0-windows\Genius.Atom.UI.Forms.TestingUtil.dll + ..\..\atom\Atom.UI.Forms.TestingUtil\bin\Debug\net6.0-windows\Genius.Atom.UI.Forms.TestingUtil.dll - ..\..\atom\Atom.UI.Forms.Demo\bin\Debug\net5.0-windows\DotNetProjects.Input.Toolkit.dll + ..\..\atom\Atom.UI.Forms.Demo\bin\Debug\net6.0-windows\DotNetProjects.Input.Toolkit.dll - ..\..\atom\Atom.UI.Forms.Demo\bin\Debug\net5.0-windows\WpfAnimatedGif.dll + ..\..\atom\Atom.UI.Forms.Demo\bin\Debug\net6.0-windows\WpfAnimatedGif.dll diff --git a/PriceChecker.UI.Tests/Validation/MustBeUniqueValidationRuleTests.cs b/PriceChecker.UI.Tests/Validation/MustBeUniqueValidationRuleTests.cs index 181dadc..534e516 100644 --- a/PriceChecker.UI.Tests/Validation/MustBeUniqueValidationRuleTests.cs +++ b/PriceChecker.UI.Tests/Validation/MustBeUniqueValidationRuleTests.cs @@ -6,73 +6,72 @@ using Genius.PriceChecker.UI.Validation; using Xunit; -namespace Genius.PriceChecker.UI.Tests.Validation +namespace Genius.PriceChecker.UI.Tests.Validation; + +public class MustBeUniqueValidationRuleTests { - public class MustBeUniqueValidationRuleTests - { - private readonly Fixture _fixture = new(); - private readonly MustBeUniqueValidationRule _sut; - private readonly TestViewModel _testVm = new(); + private readonly Fixture _fixture = new(); + private readonly MustBeUniqueValidationRule _sut; + private readonly TestViewModel _testVm = new(); - public MustBeUniqueValidationRuleTests() - { - _sut = new MustBeUniqueValidationRule(_testVm, nameof(TestViewModel.SampleSet)); - } + public MustBeUniqueValidationRuleTests() + { + _sut = new MustBeUniqueValidationRule(_testVm, nameof(TestViewModel.SampleSet)); + } - [Fact] - public void Value__Not_string__Returns_valid() - { - // Act - var result = _sut.Validate(new object(), _fixture.Create()); + [Fact] + public void Value__Not_string__Returns_valid() + { + // Act + var result = _sut.Validate(new object(), _fixture.Create()); - // Verify - Assert.True(result.IsValid); - } + // Verify + Assert.True(result.IsValid); + } - [Fact] - public void Value__Already_exists_twice_in_collection__Returns_not_valid() - { - // Arrange - _testVm.SampleSet = _fixture.CreateMany().ToList(); - _testVm.SampleSet.Add(_testVm.SampleSet[1]); + [Fact] + public void Value__Already_exists_twice_in_collection__Returns_not_valid() + { + // Arrange + _testVm.SampleSet = _fixture.CreateMany().ToList(); + _testVm.SampleSet.Add(_testVm.SampleSet[1]); - // Act - var result = _sut.Validate(_testVm.SampleSet[1], _fixture.Create()); + // Act + var result = _sut.Validate(_testVm.SampleSet[1], _fixture.Create()); - // Verify - Assert.False(result.IsValid); - } + // Verify + Assert.False(result.IsValid); + } - [Fact] - public void Value__Only_one_exists_in_collection__Returns_valid() - { - // Arrange - _testVm.SampleSet = _fixture.CreateMany().ToList(); + [Fact] + public void Value__Only_one_exists_in_collection__Returns_valid() + { + // Arrange + _testVm.SampleSet = _fixture.CreateMany().ToList(); - // Act - var result = _sut.Validate(_testVm.SampleSet[1], _fixture.Create()); + // Act + var result = _sut.Validate(_testVm.SampleSet[1], _fixture.Create()); - // Verify - Assert.True(result.IsValid); - } + // Verify + Assert.True(result.IsValid); + } - [Fact] - public void Value__Not_exists_in_collection__Returns_valid() - { - // Arrange - _testVm.SampleSet = _fixture.CreateMany().ToList(); - var valueToValidate = _fixture.Create(); + [Fact] + public void Value__Not_exists_in_collection__Returns_valid() + { + // Arrange + _testVm.SampleSet = _fixture.CreateMany().ToList(); + var valueToValidate = _fixture.Create(); - // Act - var result = _sut.Validate(valueToValidate, _fixture.Create()); + // Act + var result = _sut.Validate(valueToValidate, _fixture.Create()); - // Verify - Assert.True(result.IsValid); - } + // Verify + Assert.True(result.IsValid); + } - class TestViewModel : ViewModelBase - { - public List SampleSet { get; set; } - } + internal class TestViewModel : ViewModelBase + { + public List SampleSet { get; set; } } } diff --git a/PriceChecker.UI.Tests/Validation/ValueCannotBeEmptyValidationRuleTests.cs b/PriceChecker.UI.Tests/Validation/ValueCannotBeEmptyValidationRuleTests.cs index 468b90d..6f3ac25 100644 --- a/PriceChecker.UI.Tests/Validation/ValueCannotBeEmptyValidationRuleTests.cs +++ b/PriceChecker.UI.Tests/Validation/ValueCannotBeEmptyValidationRuleTests.cs @@ -3,83 +3,82 @@ using Genius.PriceChecker.UI.Validation; using Xunit; -namespace Genius.PriceChecker.UI.Tests.Validation +namespace Genius.PriceChecker.UI.Tests.Validation; + +public class ValueCannotBeEmptyValidationRuleTests { - public class ValueCannotBeEmptyValidationRuleTests + private readonly Fixture _fixture = new(); + private readonly ValueCannotBeEmptyValidationRule _sut = new(); + + [Fact] + public void Value__Null__Returns_not_valid() + { + // Act + var result = _sut.Validate(null, _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value__Not_string__Returns_not_valid() + { + // Act + var result = _sut.Validate(new object(), _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value__Null_string__Returns_not_valid() { - private readonly Fixture _fixture = new(); - private readonly ValueCannotBeEmptyValidationRule _sut = new(); - - [Fact] - public void Value__Null__Returns_not_valid() - { - // Act - var result = _sut.Validate(null, _fixture.Create()); - - // Verify - Assert.False(result.IsValid); - } - - [Fact] - public void Value__Not_string__Returns_not_valid() - { - // Act - var result = _sut.Validate(new object(), _fixture.Create()); - - // Verify - Assert.False(result.IsValid); - } - - [Fact] - public void Value__Null_string__Returns_not_valid() - { - // Arrange - const string value = null; - - // Act - var result = _sut.Validate(value, _fixture.Create()); - - // Verify - Assert.False(result.IsValid); - } - - [Fact] - public void Value__Empty_string__Returns_not_valid() - { - // Arrange - string value = string.Empty; - - // Act - var result = _sut.Validate(value, _fixture.Create()); - - // Verify - Assert.False(result.IsValid); - } - - [Fact] - public void Value__Whitespaced_string__Returns_not_valid() - { - // Arrange - const string value = " "; - - // Act - var result = _sut.Validate(value, _fixture.Create()); - - // Verify - Assert.False(result.IsValid); - } - - [Fact] - public void Value__String__Returns_valid() - { - // Arrange - string value = _fixture.Create(); - - // Act - var result = _sut.Validate(value, _fixture.Create()); - - // Verify - Assert.True(result.IsValid); - } + // Arrange + const string? value = null; + + // Act + var result = _sut.Validate(value, _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value__Empty_string__Returns_not_valid() + { + // Arrange + string value = string.Empty; + + // Act + var result = _sut.Validate(value, _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value__Whitespaced_string__Returns_not_valid() + { + // Arrange + const string value = " "; + + // Act + var result = _sut.Validate(value, _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value__String__Returns_valid() + { + // Arrange + string value = _fixture.Create(); + + // Act + var result = _sut.Validate(value, _fixture.Create()); + + // Verify + Assert.True(result.IsValid); } } diff --git a/PriceChecker.UI.Tests/ViewModels/MainViewModelTests.cs b/PriceChecker.UI.Tests/ViewModels/MainViewModelTests.cs index f29f271..5c2eff6 100644 --- a/PriceChecker.UI.Tests/ViewModels/MainViewModelTests.cs +++ b/PriceChecker.UI.Tests/ViewModels/MainViewModelTests.cs @@ -8,132 +8,131 @@ using Moq; using Xunit; -namespace Genius.PriceChecker.UI.Tests.ViewModels +namespace Genius.PriceChecker.UI.Tests.ViewModels; + +public class MainViewModelTests : TestBase +{ + private readonly TabMock _trackerMock = new(); + private readonly TabMock _agentsMock = new(); + private readonly TabMock _settingsMock = new(); + private readonly TabMock _logsMock = new(); + private readonly Mock _scanContextMock = new(); + private readonly Mock _notifyViewModelMock = new(); + + private readonly MainViewModel _sut; + + // Session values: + private readonly Subject<(TrackerScanStatus Status, double Progress)> _scanProgressSubject = new(); + + public MainViewModelTests() + { + _scanContextMock.SetupGet(x => x.ScanProgress).Returns(_scanProgressSubject); + + _sut = new(_trackerMock.Object, _agentsMock.Object, _settingsMock.Object, + _logsMock.Object, _scanContextMock.Object, _notifyViewModelMock.Object); + } + + [Fact] + public void Constructor__Tabs_are_populated() + { + // Verify + Assert.Equal(4, _sut.Tabs.Count); + Assert.Equal(_trackerMock.Object, _sut.Tabs[0]); + Assert.Equal(_agentsMock.Object, _sut.Tabs[1]); + Assert.Equal(_settingsMock.Object, _sut.Tabs[2]); + Assert.Equal(_logsMock.Object, _sut.Tabs[3]); + } + + [Fact] + public void SelectedTabIndex_changed__Tab_is_Activated_and_old_deactivated() + { + // Arrange + _sut.SelectedTabIndex = 0; + _trackerMock.DropHistory(); + _agentsMock.DropHistory(); + _settingsMock.DropHistory(); + _logsMock.DropHistory(); + + // Act + const int settingsTabIndex = 2; + _sut.SelectedTabIndex = settingsTabIndex; + + // Verify + Assert.True(_trackerMock.OnlyOneDeactivated); + Assert.Equal(0, _agentsMock.ActivatedCalls + _agentsMock.DeactivatedCalls); + Assert.True(_settingsMock.OnlyOneActivated); + Assert.Equal(0, _logsMock.ActivatedCalls + _logsMock.DeactivatedCalls); + } + + [Fact] + public void ScanProgress_changed__InProgress__Progress_state_highlighted_green() + { + // Arrange + _sut.ProgressState = TaskbarItemProgressState.None; + var progress = Fixture.Create(); + + // Act + _scanProgressSubject.OnNext((TrackerScanStatus.InProgress, progress)); + + // Verify + Assert.Equal(TaskbarItemProgressState.Normal, _sut.ProgressState); + Assert.Equal(progress, _sut.ProgressValue); + } + + [Fact] + public void ScanProgress_changed__InProgressWithErrors__Progress_state_highlighted_yellow() + { + // Arrange + _sut.ProgressState = TaskbarItemProgressState.None; + var progress = Fixture.Create(); + + // Act + _scanProgressSubject.OnNext((TrackerScanStatus.InProgressWithErrors, progress)); + + // Verify + Assert.Equal(TaskbarItemProgressState.Paused, _sut.ProgressState); + Assert.Equal(progress, _sut.ProgressValue); + } + + [Fact] + public void ScanProgress_changed__Finished__Progress_state_dropped_and_message_shown() + { + // Arrange + _sut.ProgressState = TaskbarItemProgressState.Normal; + _sut.ProgressValue = Fixture.Create(); + + // Act + _scanProgressSubject.OnNext((TrackerScanStatus.Finished, Fixture.Create())); + + // Verify + Assert.Equal(TaskbarItemProgressState.None, _sut.ProgressState); + Assert.Equal(0, _sut.ProgressValue); + } +} + +internal class TabMock : Mock + where T: class, ITabViewModel { - public class MainViewModelTests : TestBase + public int ActivatedCalls = 0; + public int DeactivatedCalls = 0; + + public TabMock() { - private readonly TabMock _trackerMock = new(); - private readonly TabMock _agentsMock = new(); - private readonly TabMock _settingsMock = new(); - private readonly TabMock _logsMock = new(); - private readonly Mock _scanContextMock = new(); - private readonly Mock _notifyViewModelMock = new(); - - private readonly MainViewModel _sut; - - // Session values: - private readonly Subject<(TrackerScanStatus Status, double Progress)> _scanProgressSubject = new(); - - public MainViewModelTests() - { - _scanContextMock.SetupGet(x => x.ScanProgress).Returns(_scanProgressSubject); - - _sut = new(_trackerMock.Object, _agentsMock.Object, _settingsMock.Object, - _logsMock.Object, _scanContextMock.Object, _notifyViewModelMock.Object); - } - - [Fact] - public void Constructor__Tabs_are_populated() - { - // Verify - Assert.Equal(4, _sut.Tabs.Count); - Assert.Equal(_trackerMock.Object, _sut.Tabs[0]); - Assert.Equal(_agentsMock.Object, _sut.Tabs[1]); - Assert.Equal(_settingsMock.Object, _sut.Tabs[2]); - Assert.Equal(_logsMock.Object, _sut.Tabs[3]); - } - - [Fact] - public void SelectedTabIndex_changed__Tab_is_Activated_and_old_deactivated() - { - // Arrange - _sut.SelectedTabIndex = 0; - _trackerMock.DropHistory(); - _agentsMock.DropHistory(); - _settingsMock.DropHistory(); - _logsMock.DropHistory(); - - // Act - const int settingsTabIndex = 2; - _sut.SelectedTabIndex = settingsTabIndex; - - // Verify - Assert.True(_trackerMock.OnlyOneDeactivated); - Assert.Equal(0, _agentsMock.ActivatedCalls + _agentsMock.DeactivatedCalls); - Assert.True(_settingsMock.OnlyOneActivated); - Assert.Equal(0, _logsMock.ActivatedCalls + _logsMock.DeactivatedCalls); - } - - [Fact] - public void ScanProgress_changed__InProgress__Progress_state_highlighted_green() - { - // Arrange - _sut.ProgressState = TaskbarItemProgressState.None; - var progress = Fixture.Create(); - - // Act - _scanProgressSubject.OnNext((TrackerScanStatus.InProgress, progress)); - - // Verify - Assert.Equal(TaskbarItemProgressState.Normal, _sut.ProgressState); - Assert.Equal(progress, _sut.ProgressValue); - } - - [Fact] - public void ScanProgress_changed__InProgressWithErrors__Progress_state_highlighted_yellow() - { - // Arrange - _sut.ProgressState = TaskbarItemProgressState.None; - var progress = Fixture.Create(); - - // Act - _scanProgressSubject.OnNext((TrackerScanStatus.InProgressWithErrors, progress)); - - // Verify - Assert.Equal(TaskbarItemProgressState.Paused, _sut.ProgressState); - Assert.Equal(progress, _sut.ProgressValue); - } - - [Fact] - public void ScanProgress_changed__Finished__Progress_state_dropped_and_message_shown() - { - // Arrange - _sut.ProgressState = TaskbarItemProgressState.Normal; - _sut.ProgressValue = Fixture.Create(); - - // Act - _scanProgressSubject.OnNext((TrackerScanStatus.Finished, Fixture.Create())); - - // Verify - Assert.Equal(TaskbarItemProgressState.None, _sut.ProgressState); - Assert.Equal(0, _sut.ProgressValue); - } + var activatedCommandMock = new Mock(); + activatedCommandMock.Setup(x => x.Execute(null)).Callback((object _) => ActivatedCalls++); + SetupGet(x => x.Activated).Returns(activatedCommandMock.Object); + + var deactivatedCommandMock = new Mock(); + deactivatedCommandMock.Setup(x => x.Execute(null)).Callback((object _) => DeactivatedCalls++); + SetupGet(x => x.Deactivated).Returns(deactivatedCommandMock.Object); } - internal class TabMock : Mock - where T: class, ITabViewModel + public void DropHistory() { - public int ActivatedCalls = 0; - public int DeactivatedCalls = 0; - - public TabMock() - { - var activatedCommandMock = new Mock(); - activatedCommandMock.Setup(x => x.Execute(null)).Callback((object _) => ActivatedCalls++); - SetupGet(x => x.Activated).Returns(activatedCommandMock.Object); - - var deactivatedCommandMock = new Mock(); - deactivatedCommandMock.Setup(x => x.Execute(null)).Callback((object _) => DeactivatedCalls++); - SetupGet(x => x.Deactivated).Returns(deactivatedCommandMock.Object); - } - - public void DropHistory() - { - ActivatedCalls = 0; - DeactivatedCalls = 0; - } - - public bool OnlyOneActivated => ActivatedCalls == 1 && DeactivatedCalls == 0; - public bool OnlyOneDeactivated => ActivatedCalls == 0 && DeactivatedCalls == 1; + ActivatedCalls = 0; + DeactivatedCalls = 0; } + + public bool OnlyOneActivated => ActivatedCalls == 1 && DeactivatedCalls == 0; + public bool OnlyOneDeactivated => ActivatedCalls == 0 && DeactivatedCalls == 1; } diff --git a/PriceChecker.UI.Tests/ViewModels/TrackerViewModelTests.cs b/PriceChecker.UI.Tests/ViewModels/TrackerViewModelTests.cs index 65cd8f8..876ca85 100644 --- a/PriceChecker.UI.Tests/ViewModels/TrackerViewModelTests.cs +++ b/PriceChecker.UI.Tests/ViewModels/TrackerViewModelTests.cs @@ -16,339 +16,338 @@ using Moq; using Xunit; -namespace Genius.PriceChecker.UI.Tests.ViewModels +namespace Genius.PriceChecker.UI.Tests.ViewModels; + +public class TrackerViewModelTests : TestBase { - public class TrackerViewModelTests : TestBase + private readonly Mock _productQueryMock = new(); + private readonly Mock _vmFactoryMock = new(); + private readonly Mock _uiMock = new(); + private readonly Mock _scanContextMock = new(); + private readonly Mock _commandBusMock = new(); + + // Session values: + private readonly Subject _productScanStartedEventSubject; + private readonly Subject _productScannedEventSubject; + private readonly Subject _productScanFailedEventSubject; + + public TrackerViewModelTests() { - private readonly Mock _productQueryMock = new(); - private readonly Mock _vmFactoryMock = new(); - private readonly Mock _uiMock = new(); - private readonly Mock _scanContextMock = new(); - private readonly Mock _commandBusMock = new(); - - // Session values: - private readonly Subject _productScanStartedEventSubject; - private readonly Subject _productScannedEventSubject; - private readonly Subject _productScanFailedEventSubject; - - public TrackerViewModelTests() - { - _productScanStartedEventSubject = CreateEventSubject(); - _productScannedEventSubject = CreateEventSubject(); - _productScanFailedEventSubject = CreateEventSubject(); - - _vmFactoryMock.Setup(x => x.CreateTrackerProduct(It.IsAny())) - .Returns((Product p) => Mock.Of(x => - x.Id == (p == null ? Guid.Empty : p.Id) && - x.RefreshPriceCommand == Mock.Of() && - x.CommitProductCommand == Mock.Of(c => c.Executed == new Subject()))); - } + _productScanStartedEventSubject = CreateEventSubject(); + _productScannedEventSubject = CreateEventSubject(); + _productScanFailedEventSubject = CreateEventSubject(); + + _vmFactoryMock.Setup(x => x.CreateTrackerProduct(It.IsAny())) + .Returns((Product p) => Mock.Of(x => + x.Id == (p == null ? Guid.Empty : p.Id) && + x.RefreshPriceCommand == Mock.Of() && + x.CommitProductCommand == Mock.Of(c => c.Executed == new Subject()))); + } - [Fact] - public void Constructor__RefreshOptions_are_defined_and_list_reloaded() - { - // Arrange - var products = SampleProducts(); + [Fact] + public void Constructor__RefreshOptions_are_defined_and_list_reloaded() + { + // Arrange + var products = SampleProducts(); - // Act - var sut = CreateSystemUnderTest(); + // Act + var sut = CreateSystemUnderTest(); - // Verify - Assert.NotEmpty(sut.RefreshOptions); - Assert.Equal(products.Select(x => x.Id), sut.Products.Select(x => x.Id)); - } + // Verify + Assert.NotEmpty(sut.RefreshOptions); + Assert.Equal(products.Select(x => x.Id), sut.Products.Select(x => x.Id)); + } - [Fact] - public void RefreshAllCommand__Enqueues_all_products_for_scan() + [Fact] + public void RefreshAllCommand__Enqueues_all_products_for_scan() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.IsAddEditProductVisible = true; + + // Act + sut.RefreshAllCommand.Execute(null); + + // Verify + Assert.False(sut.IsAddEditProductVisible); + _scanContextMock.Verify(x => x.NotifyStarted(products.Count)); + foreach (var product in sut.Products) { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.IsAddEditProductVisible = true; - - // Act - sut.RefreshAllCommand.Execute(null); - - // Verify - Assert.False(sut.IsAddEditProductVisible); - _scanContextMock.Verify(x => x.NotifyStarted(products.Count)); - foreach (var product in sut.Products) - { - Mock.Get(product.RefreshPriceCommand).Verify(x => x.Execute(null), Times.Once); - } + Mock.Get(product.RefreshPriceCommand).Verify(x => x.Execute(null), Times.Once); } + } - [Fact] - public void RefreshAllCommand__Enqueues_selected_products_for_scan() + [Fact] + public void RefreshAllCommand__Enqueues_selected_products_for_scan() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.Products[0].IsSelected = true; + sut.Products[2].IsSelected = true; + + // Act + sut.RefreshSelectedCommand.Execute(null); + + // Verify + _scanContextMock.Verify(x => x.NotifyStarted(2)); + foreach (var product in sut.Products) { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.Products[0].IsSelected = true; - sut.Products[2].IsSelected = true; - - // Act - sut.RefreshSelectedCommand.Execute(null); - - // Verify - _scanContextMock.Verify(x => x.NotifyStarted(2)); - foreach (var product in sut.Products) - { - var times = product == sut.Products[0] || product == sut.Products[2] ? Times.Once() : Times.Never(); - Mock.Get(product.RefreshPriceCommand).Verify(x => x.Execute(null), times); - } + var times = product == sut.Products[0] || product == sut.Products[2] ? Times.Once() : Times.Never(); + Mock.Get(product.RefreshPriceCommand).Verify(x => x.Execute(null), times); } + } - [Fact] - public void OpenAddProductFlyoutCommand__When_flyout_is_closed__Flyout_shows_up() - { - // Arrange - SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.IsAddEditProductVisible = false; - sut.EditingProduct = null; - - // Act - sut.OpenAddProductFlyoutCommand.Execute(null); - - // Verify - Assert.True(sut.IsAddEditProductVisible); - Assert.NotNull(sut.EditingProduct); - Assert.Equal(Guid.Empty, sut.EditingProduct.Id); - } + [Fact] + public void OpenAddProductFlyoutCommand__When_flyout_is_closed__Flyout_shows_up() + { + // Arrange + SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.IsAddEditProductVisible = false; + sut.EditingProduct = null; + + // Act + sut.OpenAddProductFlyoutCommand.Execute(null); + + // Verify + Assert.True(sut.IsAddEditProductVisible); + Assert.NotNull(sut.EditingProduct); + Assert.Equal(Guid.Empty, sut.EditingProduct!.Id); + } - [Fact] - public void OpenAddProductFlyoutCommand__When_flyout_is_opened__Flyout_closed() - { - // Arrange - SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.IsAddEditProductVisible = true; + [Fact] + public void OpenAddProductFlyoutCommand__When_flyout_is_opened__Flyout_closed() + { + // Arrange + SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.IsAddEditProductVisible = true; - // Act - sut.OpenAddProductFlyoutCommand.Execute(null); + // Act + sut.OpenAddProductFlyoutCommand.Execute(null); - // Verify - Assert.False(sut.IsAddEditProductVisible); - } + // Verify + Assert.False(sut.IsAddEditProductVisible); + } - [Fact] - public void OpenAddProductFlyoutCommand__Product_committed__List_reloaded_and_flyout_closed() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.OpenAddProductFlyoutCommand.Execute(null); // trigger to open flyout + [Fact] + public void OpenAddProductFlyoutCommand__Product_committed__List_reloaded_and_flyout_closed() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.OpenAddProductFlyoutCommand.Execute(null); // trigger to open flyout - // Act - ((Subject)sut.EditingProduct.CommitProductCommand.Executed).OnNext(Unit.Default); + // Act + ((Subject)sut.EditingProduct.CommitProductCommand.Executed).OnNext(Unit.Default); - // Verify - Assert.False(sut.IsAddEditProductVisible); - _productQueryMock.Verify(x => x.GetAll(), Times.Exactly(2)); - } + // Verify + Assert.False(sut.IsAddEditProductVisible); + _productQueryMock.Verify(x => x.GetAll(), Times.Exactly(2)); + } - [Fact] - public void OpenEditProductFlyoutCommand__When_flyout_is_closed__Flyout_shows_up() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.IsAddEditProductVisible = false; - sut.Products[0].IsSelected = true; - sut.EditingProduct = null; - - // Act - sut.OpenEditProductFlyoutCommand.Execute(null); - - // Verify - Assert.True(sut.IsAddEditProductVisible); - Assert.NotNull(sut.EditingProduct); - Assert.Equal(products.First().Id, sut.EditingProduct.Id); - } + [Fact] + public void OpenEditProductFlyoutCommand__When_flyout_is_closed__Flyout_shows_up() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.IsAddEditProductVisible = false; + sut.Products[0].IsSelected = true; + sut.EditingProduct = null; + + // Act + sut.OpenEditProductFlyoutCommand.Execute(null); + + // Verify + Assert.True(sut.IsAddEditProductVisible); + Assert.NotNull(sut.EditingProduct); + Assert.Equal(products.First().Id, sut.EditingProduct!.Id); + } - [Fact] - public void OpenEditProductFlyoutCommand__When_flyout_is_opened__Flyout_closed() - { - // Arrange - SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.IsAddEditProductVisible = true; + [Fact] + public void OpenEditProductFlyoutCommand__When_flyout_is_opened__Flyout_closed() + { + // Arrange + SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.IsAddEditProductVisible = true; - // Act - sut.OpenEditProductFlyoutCommand.Execute(null); + // Act + sut.OpenEditProductFlyoutCommand.Execute(null); - // Verify - Assert.False(sut.IsAddEditProductVisible); - } + // Verify + Assert.False(sut.IsAddEditProductVisible); + } - [Fact] - public void OpenEditProductFlyoutCommand__Product_committed__Flyout_closed() - { - // Arrange - SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.Products[0].IsSelected = true; - sut.OpenEditProductFlyoutCommand.Execute(null); // trigger to open flyout + [Fact] + public void OpenEditProductFlyoutCommand__Product_committed__Flyout_closed() + { + // Arrange + SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.Products[0].IsSelected = true; + sut.OpenEditProductFlyoutCommand.Execute(null); // trigger to open flyout - // Act - ((Subject)sut.EditingProduct.CommitProductCommand.Executed).OnNext(Unit.Default); + // Act + ((Subject)sut.EditingProduct.CommitProductCommand.Executed).OnNext(Unit.Default); - // Verify - Assert.False(sut.IsAddEditProductVisible); - } + // Verify + Assert.False(sut.IsAddEditProductVisible); + } - [Fact] - public void DeleteProductCommand__User_confirmed__Flyout_closed_and_product_removed() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.Products[1].IsSelected = true; - sut.IsAddEditProductVisible = true; - _uiMock.Setup(x => x.AskForConfirmation(It.IsAny(), It.IsAny())).Returns(true); - - // Act - sut.DeleteProductCommand.Execute(null); - - // Verify - var deletedProductId = products.ElementAt(1).Id; - Assert.False(sut.IsAddEditProductVisible); - Assert.DoesNotContain(sut.Products, x => x.Id == deletedProductId); - Assert.Equal(products.Count - 1, sut.Products.Count); - _commandBusMock.Verify(x => x.SendAsync(It.Is(c => c.ProductId == deletedProductId))); - } + [Fact] + public void DeleteProductCommand__User_confirmed__Flyout_closed_and_product_removed() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.Products[1].IsSelected = true; + sut.IsAddEditProductVisible = true; + _uiMock.Setup(x => x.AskForConfirmation(It.IsAny(), It.IsAny())).Returns(true); + + // Act + sut.DeleteProductCommand.Execute(null); + + // Verify + var deletedProductId = products.ElementAt(1).Id; + Assert.False(sut.IsAddEditProductVisible); + Assert.DoesNotContain(sut.Products, x => x.Id == deletedProductId); + Assert.Equal(products.Count - 1, sut.Products.Count); + _commandBusMock.Verify(x => x.SendAsync(It.Is(c => c.ProductId == deletedProductId))); + } - [Fact] - public void DeleteProductCommand__User_not_confirmed__Flyout_closed_and_product_remained() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.Products[1].IsSelected = true; - sut.IsAddEditProductVisible = true; - _uiMock.Setup(x => x.AskForConfirmation(It.IsAny(), It.IsAny())).Returns(false); - - // Act - sut.DeleteProductCommand.Execute(null); - - // Verify - var deletingProductId = products.ElementAt(1).Id; - Assert.False(sut.IsAddEditProductVisible); - Assert.Contains(sut.Products, x => x.Id == deletingProductId); - Assert.Equal(products.Count, sut.Products.Count); - _commandBusMock.Verify(x => x.SendAsync(It.IsAny()), Times.Never); - } + [Fact] + public void DeleteProductCommand__User_not_confirmed__Flyout_closed_and_product_remained() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.Products[1].IsSelected = true; + sut.IsAddEditProductVisible = true; + _uiMock.Setup(x => x.AskForConfirmation(It.IsAny(), It.IsAny())).Returns(false); + + // Act + sut.DeleteProductCommand.Execute(null); + + // Verify + var deletingProductId = products.ElementAt(1).Id; + Assert.False(sut.IsAddEditProductVisible); + Assert.Contains(sut.Products, x => x.Id == deletingProductId); + Assert.Equal(products.Count, sut.Products.Count); + _commandBusMock.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + } - [Fact] - public void DeleteProductCommand__No_product_selected__Flyout_closed_and_operation_cancelled() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.IsAddEditProductVisible = true; - - // Act - sut.DeleteProductCommand.Execute(null); - - // Verify - Assert.False(sut.IsAddEditProductVisible); - Assert.Equal(products.Count, sut.Products.Count); - _commandBusMock.Verify(x => x.SendAsync(It.IsAny()), Times.Never); - _uiMock.Verify(x => x.AskForConfirmation(It.IsAny(), It.IsAny()), Times.Never); - } + [Fact] + public void DeleteProductCommand__No_product_selected__Flyout_closed_and_operation_cancelled() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.IsAddEditProductVisible = true; + + // Act + sut.DeleteProductCommand.Execute(null); + + // Verify + Assert.False(sut.IsAddEditProductVisible); + Assert.Equal(products.Count, sut.Products.Count); + _commandBusMock.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + _uiMock.Verify(x => x.AskForConfirmation(It.IsAny(), It.IsAny()), Times.Never); + } - [Fact] - public void ProductScanStartedEvent_fired__Appropriate_product_changed_status() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - const int productScanningIndex = 1; + [Fact] + public void ProductScanStartedEvent_fired__Appropriate_product_changed_status() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + const int productScanningIndex = 1; - // Act - _productScanStartedEventSubject.OnNext(new ProductScanStartedEvent(products.ElementAt(productScanningIndex))); + // Act + _productScanStartedEventSubject.OnNext(new ProductScanStartedEvent(products.ElementAt(productScanningIndex))); - // Verify - Assert.Equal(ProductScanStatus.Scanning, sut.Products[productScanningIndex].Status); - } + // Verify + Assert.Equal(ProductScanStatus.Scanning, sut.Products[productScanningIndex].Status); + } - [Fact] - public void ProductScannedEvent_fired__Appropriate_product_changed_status() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - var lowestUpdated = Fixture.Create(); - const int productScannedIndex = 1; + [Fact] + public void ProductScannedEvent_fired__Appropriate_product_changed_status() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + var lowestUpdated = Fixture.Create(); + const int productScannedIndex = 1; - // Act - _productScannedEventSubject.OnNext(new ProductScannedEvent(products.ElementAt(productScannedIndex), lowestUpdated)); + // Act + _productScannedEventSubject.OnNext(new ProductScannedEvent(products.ElementAt(productScannedIndex), lowestUpdated)); - // Verify - Mock.Get(sut.Products[productScannedIndex]).Verify(x => x.Reconcile(lowestUpdated), Times.Once); - } + // Verify + Mock.Get(sut.Products[productScannedIndex]).Verify(x => x.Reconcile(lowestUpdated), Times.Once); + } - [Fact] - public void ProductScanFailedEvent_fired__Appropriate_product_set_to_failed() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - const int productFailedIndex = 1; - var errorMessage = Fixture.Create(); + [Fact] + public void ProductScanFailedEvent_fired__Appropriate_product_set_to_failed() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + const int productFailedIndex = 1; + var errorMessage = Fixture.Create(); - // Act - _productScanFailedEventSubject.OnNext(new ProductScanFailedEvent(products.ElementAt(productFailedIndex), errorMessage)); + // Act + _productScanFailedEventSubject.OnNext(new ProductScanFailedEvent(products.ElementAt(productFailedIndex), errorMessage)); - // Verify - Mock.Get(sut.Products[productFailedIndex]).Verify(x => x.SetFailed(errorMessage), Times.Once); - } + // Verify + Mock.Get(sut.Products[productFailedIndex]).Verify(x => x.SetFailed(errorMessage), Times.Once); + } - [Fact] - public void Deactivated__Flyout_closed() - { - // Arrange - SampleProducts(); - var sut = CreateSystemUnderTest(); - sut.IsAddEditProductVisible = true; + [Fact] + public void Deactivated__Flyout_closed() + { + // Arrange + SampleProducts(); + var sut = CreateSystemUnderTest(); + sut.IsAddEditProductVisible = true; - // Act - sut.Deactivated.Execute(null); + // Act + sut.Deactivated.Execute(null); - // Verify - Assert.False(sut.IsAddEditProductVisible); - } + // Verify + Assert.False(sut.IsAddEditProductVisible); + } - [Fact] - public void Product_status_changed__Scan_context_notified() - { - // Arrange - var products = SampleProducts(); - var sut = CreateSystemUnderTest(); - var product = sut.Products[1]; - var status = Fixture.Create>() - .First(x => x != product.Status); - - // Act - RaisePropertyChanged(Mock.Get(product), x => x.Status, status); - - // Verify - _scanContextMock.Verify(x => x.NotifyProgressChange(status)); - } + [Fact] + public void Product_status_changed__Scan_context_notified() + { + // Arrange + var products = SampleProducts(); + var sut = CreateSystemUnderTest(); + var product = sut.Products[1]; + var status = Fixture.Create>() + .First(x => x != product.Status); + + // Act + RaisePropertyChanged(Mock.Get(product), x => x.Status, status); + + // Verify + _scanContextMock.Verify(x => x.NotifyProgressChange(status)); + } - private TrackerViewModel CreateSystemUnderTest() - { - return new TrackerViewModel(EventBusMock.Object, _productQueryMock.Object, - _vmFactoryMock.Object, _uiMock.Object, _scanContextMock.Object, - _commandBusMock.Object); - } + private TrackerViewModel CreateSystemUnderTest() + { + return new TrackerViewModel(EventBusMock.Object, _productQueryMock.Object, + _vmFactoryMock.Object, _uiMock.Object, _scanContextMock.Object, + _commandBusMock.Object); + } - private ICollection SampleProducts() - { - var products = Fixture.CreateMany().ToList(); - _productQueryMock.Setup(x => x.GetAll()).Returns(products); - return products; - } + private ICollection SampleProducts() + { + var products = Fixture.CreateMany().ToList(); + _productQueryMock.Setup(x => x.GetAll()).Returns(products); + return products; } } diff --git a/PriceChecker.UI/App.xaml.cs b/PriceChecker.UI/App.xaml.cs index e5e3d7e..16a96b6 100644 --- a/PriceChecker.UI/App.xaml.cs +++ b/PriceChecker.UI/App.xaml.cs @@ -1,83 +1,82 @@ -using System; +global using System; +global using Genius.Atom.Infrastructure; + using System.Diagnostics.CodeAnalysis; using System.Windows; using Genius.PriceChecker.Core.Services; -using Genius.Atom.Infrastructure.Events; -using Genius.Atom.Infrastructure.Logging; using Genius.PriceChecker.UI.Helpers; using Genius.PriceChecker.UI.ViewModels; using Genius.PriceChecker.UI.Views; using Hardcodet.Wpf.TaskbarNotification; using Microsoft.Extensions.DependencyInjection; -namespace Genius.PriceChecker.UI -{ - [ExcludeFromCodeCoverage] - public partial class App : Application - { - private TaskbarIcon _notifyIcon; - - public static IServiceProvider ServiceProvider { get; private set; } +namespace Genius.PriceChecker.UI; - protected override void OnStartup(StartupEventArgs e) - { - base.OnStartup(e); - - _notifyIcon = (TaskbarIcon) FindResource("NotifyIcon"); +[ExcludeFromCodeCoverage] +public partial class App : Application +{ + private TaskbarIcon _notifyIcon; - var serviceCollection = new ServiceCollection(); - ConfigureServices(serviceCollection); + public static IServiceProvider ServiceProvider { get; private set; } - serviceCollection.AddSingleton((NotifyIconViewModel)_notifyIcon.DataContext); + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); - ServiceProvider = serviceCollection.BuildServiceProvider(); - Core.Module.Initialize(ServiceProvider); - Atom.UI.Forms.Module.Initialize(ServiceProvider); + _notifyIcon = (TaskbarIcon) FindResource("NotifyIcon"); - var mainWindow = ServiceProvider.GetRequiredService(); - mainWindow.Show(); - } + var serviceCollection = new ServiceCollection(); + ConfigureServices(serviceCollection); - protected override void OnExit(ExitEventArgs e) - { - base.OnExit(e); + serviceCollection.AddSingleton((NotifyIconViewModel)_notifyIcon.DataContext); - var manager = ServiceProvider.GetRequiredService(); - manager.Dispose(); + ServiceProvider = serviceCollection.BuildServiceProvider(); + Core.Module.Initialize(ServiceProvider); + Atom.UI.Forms.Module.Initialize(ServiceProvider); - _notifyIcon.Dispose(); - } + var mainWindow = ServiceProvider.GetRequiredService(); + mainWindow.Show(); + } - private static void ConfigureServices(IServiceCollection services) - { - Atom.Infrastructure.Module.Configure(services); - Core.Module.Configure(services); + protected override void OnExit(ExitEventArgs e) + { + base.OnExit(e); - // Framework: - services.AddLogging(); + var manager = ServiceProvider.GetRequiredService(); + manager.Dispose(); - // Views, View models, and the View model factory - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + _notifyIcon.Dispose(); + } - // Services and Helpers: - services.AddTransient(); - services.AddSingleton(); - } + private static void ConfigureServices(IServiceCollection services) + { + Atom.Infrastructure.Module.Configure(services); + Core.Module.Configure(services); + + // Framework: + services.AddLogging(); + + // Views, View models, and the View model factory + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Services and Helpers: + services.AddTransient(); + services.AddSingleton(); + } - private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) - { + private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) + { #if !DEBUG - MessageBox.Show("An unhandled exception just occurred: " + e.Exception.Message, "Unhandled Exception", MessageBoxButton.OK, MessageBoxImage.Warning); - e.Handled = true; + MessageBox.Show("An unhandled exception just occurred: " + e.Exception.Message, "Unhandled Exception", MessageBoxButton.OK, MessageBoxImage.Warning); + e.Handled = true; #endif - } } } diff --git a/PriceChecker.UI/Helpers/CollectionExtensions.cs b/PriceChecker.UI/Helpers/CollectionExtensions.cs index ac5abb9..1195bbc 100644 --- a/PriceChecker.UI/Helpers/CollectionExtensions.cs +++ b/PriceChecker.UI/Helpers/CollectionExtensions.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; -namespace Genius.PriceChecker.UI.Helpers +namespace Genius.PriceChecker.UI.Helpers; + +public static class CollectionExtensions { - public static class CollectionExtensions + public static void ReplaceItems(this ICollection collection, IEnumerable items) { - public static void ReplaceItems(this ICollection collection, IEnumerable items) + collection.Clear(); + foreach (var item in items) { - collection.Clear(); - foreach (var item in items) - { - collection.Add(item); - } + collection.Add(item); } } } diff --git a/PriceChecker.UI/Helpers/ResourcesHelper.cs b/PriceChecker.UI/Helpers/ResourcesHelper.cs index 6e14d28..8bdef35 100644 --- a/PriceChecker.UI/Helpers/ResourcesHelper.cs +++ b/PriceChecker.UI/Helpers/ResourcesHelper.cs @@ -1,26 +1,25 @@ using System.Windows.Media.Imaging; using Genius.PriceChecker.Core.Models; -namespace Genius.PriceChecker.UI.Helpers +namespace Genius.PriceChecker.UI.Helpers; + +public static class ResourcesHelper { - public static class ResourcesHelper + public static BitmapImage? GetStatusIcon(ProductScanStatus status) { - public static BitmapImage GetStatusIcon(ProductScanStatus status) + var icon = status switch { - var icon = status switch - { - ProductScanStatus.NotScanned => "Unknown16", - ProductScanStatus.Scanning => "Loading32", - ProductScanStatus.ScannedOk => "DonePink16", - ProductScanStatus.ScannedWithErrors => "Warning16", - ProductScanStatus.ScannedNewLowest => "Dance32", - ProductScanStatus.Outdated => "Outdated16", - ProductScanStatus.Failed => "Error16", - {} => null - }; - if (icon == null) - return null; - return (BitmapImage)App.Current.FindResource(icon); - } + ProductScanStatus.NotScanned => "Unknown16", + ProductScanStatus.Scanning => "Loading32", + ProductScanStatus.ScannedOk => "DonePink16", + ProductScanStatus.ScannedWithErrors => "Warning16", + ProductScanStatus.ScannedNewLowest => "Dance32", + ProductScanStatus.Outdated => "Outdated16", + ProductScanStatus.Failed => "Error16", + {} => null + }; + if (icon == null) + return null; + return (BitmapImage)App.Current.FindResource(icon); } } diff --git a/PriceChecker.UI/Helpers/ShowBalloonTipEventArgs.cs b/PriceChecker.UI/Helpers/ShowBalloonTipEventArgs.cs deleted file mode 100644 index b39edc9..0000000 --- a/PriceChecker.UI/Helpers/ShowBalloonTipEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Hardcodet.Wpf.TaskbarNotification; - -namespace Genius.PriceChecker.UI.Helpers -{ - public class ShowBalloonTipEventArgs - { - public string Title { get; set; } - public string Message { get; set; } - public BalloonIcon Icon { get; set; } - } -} diff --git a/PriceChecker.UI/Helpers/TrackerScanContext.cs b/PriceChecker.UI/Helpers/TrackerScanContext.cs index abce746..6fb18bf 100644 --- a/PriceChecker.UI/Helpers/TrackerScanContext.cs +++ b/PriceChecker.UI/Helpers/TrackerScanContext.cs @@ -1,91 +1,89 @@ -using System; using System.Reactive.Subjects; using Genius.PriceChecker.Core.Messages; using Genius.PriceChecker.Core.Models; using Genius.Atom.Infrastructure.Events; -namespace Genius.PriceChecker.UI.Helpers +namespace Genius.PriceChecker.UI.Helpers; + +public interface ITrackerScanContext +{ + void NotifyStarted(int count); + void NotifyProgressChange(ProductScanStatus productScanStatus); + + IObservable<(TrackerScanStatus Status, double Progress)> ScanProgress { get; } + bool HasErrors { get; } + bool HasNewLowestPrice { get; } +} + +internal sealed class TrackerScanContext : ITrackerScanContext { - public interface ITrackerScanContext + private readonly Subject<(TrackerScanStatus status, double Progress)> _scanProgress = new(); + + private bool _started; + private int _count; + private int _finished; + + public TrackerScanContext(IEventBus eventBus) { - void NotifyStarted(int count); - void NotifyProgressChange(ProductScanStatus productScanStatus); + eventBus.WhenFired() + .Subscribe(ev => + NotifyStarted(ev.ProductsCount)); + } - IObservable<(TrackerScanStatus Status, double Progress)> ScanProgress { get; } - bool HasErrors { get; } - bool HasNewLowestPrice { get; } + public void NotifyStarted(int count) + { + _started = true; + _count = count; + _finished = 0; + HasErrors = false; + HasNewLowestPrice = false; + + var initialProgress = CalculateProgress(); + _scanProgress.OnNext((TrackerScanStatus.InProgress, initialProgress)); } - internal sealed class TrackerScanContext : ITrackerScanContext + public void NotifyProgressChange(ProductScanStatus productScanStatus) { - private readonly Subject<(TrackerScanStatus status, double Progress)> _scanProgress = new(); + if (!_started) + return; - private bool _started; - private int _count; - private int _finished; + var isFinished = productScanStatus == ProductScanStatus.ScannedOk + || productScanStatus == ProductScanStatus.ScannedWithErrors + || productScanStatus == ProductScanStatus.ScannedNewLowest + || productScanStatus == ProductScanStatus.Failed; - public TrackerScanContext(IEventBus eventBus) - { - eventBus.WhenFired() - .Subscribe(ev => - NotifyStarted(ev.ProductsCount)); - } + HasErrors = HasErrors || productScanStatus == ProductScanStatus.Failed + || productScanStatus == ProductScanStatus.ScannedWithErrors; + HasNewLowestPrice = HasNewLowestPrice || productScanStatus == ProductScanStatus.ScannedNewLowest; + + if (isFinished) + _finished++; + + double progress = CalculateProgress(); - public void NotifyStarted(int count) + var status = TrackerScanStatus.InProgress; + if (_finished == _count) { - _started = true; - _count = count; - _finished = 0; - HasErrors = false; - HasNewLowestPrice = false; - - var initialProgress = CalculateProgress(); - _scanProgress.OnNext((TrackerScanStatus.InProgress, initialProgress)); + _started = false; + status = TrackerScanStatus.Finished; } - - public void NotifyProgressChange(ProductScanStatus productScanStatus) + else if (HasErrors) { - if (!_started) - return; - - var isFinished = productScanStatus == ProductScanStatus.ScannedOk - || productScanStatus == ProductScanStatus.ScannedWithErrors - || productScanStatus == ProductScanStatus.ScannedNewLowest - || productScanStatus == ProductScanStatus.Failed; - - HasErrors = HasErrors || productScanStatus == ProductScanStatus.Failed - || productScanStatus == ProductScanStatus.ScannedWithErrors; - HasNewLowestPrice = HasNewLowestPrice || productScanStatus == ProductScanStatus.ScannedNewLowest; - - if (isFinished) - _finished++; - - double progress = CalculateProgress(); - - var status = TrackerScanStatus.InProgress; - if (_finished == _count) - { - _started = false; - status = TrackerScanStatus.Finished; - } - else if (HasErrors) - { - status = TrackerScanStatus.InProgressWithErrors; - } - - _scanProgress.OnNext((status, progress)); + status = TrackerScanStatus.InProgressWithErrors; } - private double CalculateProgress() - => _finished == 0 - ? 1.0d / (_count * 2) - : 1.0d * _finished / _count; + _scanProgress.OnNext((status, progress)); + } + + private double CalculateProgress() + => _finished == 0 + ? 1.0d / (_count * 2) + : 1.0d * _finished / _count; - public IObservable<(TrackerScanStatus Status, double Progress)> ScanProgress => _scanProgress; + public IObservable<(TrackerScanStatus Status, double Progress)> ScanProgress => _scanProgress; - public bool IsStarted => _started; - public int FinishedJobs => _finished; - public bool HasErrors { get; private set; } - public bool HasNewLowestPrice { get; private set; } - } -} \ No newline at end of file + public bool IsStarted => _started; + public int FinishedJobs => _finished; + public bool HasErrors { get; private set; } + public bool HasNewLowestPrice { get; private set; } +} diff --git a/PriceChecker.UI/Helpers/TrackerScanStatus.cs b/PriceChecker.UI/Helpers/TrackerScanStatus.cs index b28995e..80654fa 100644 --- a/PriceChecker.UI/Helpers/TrackerScanStatus.cs +++ b/PriceChecker.UI/Helpers/TrackerScanStatus.cs @@ -1,10 +1,9 @@ -namespace Genius.PriceChecker.UI.Helpers +namespace Genius.PriceChecker.UI.Helpers; + +public enum TrackerScanStatus { - public enum TrackerScanStatus - { - NotStarted, - InProgress, - InProgressWithErrors, - Finished - } + NotStarted, + InProgress, + InProgressWithErrors, + Finished } diff --git a/PriceChecker.UI/Helpers/UserInteraction.cs b/PriceChecker.UI/Helpers/UserInteraction.cs index cf9f428..b3063b9 100644 --- a/PriceChecker.UI/Helpers/UserInteraction.cs +++ b/PriceChecker.UI/Helpers/UserInteraction.cs @@ -4,69 +4,68 @@ using Genius.PriceChecker.Core.Models; using Genius.PriceChecker.Core.Repositories; -namespace Genius.PriceChecker.UI.Helpers +namespace Genius.PriceChecker.UI.Helpers; + +public interface IUserInteraction { - public interface IUserInteraction - { - /// - /// Shows a message box to a user with buttons Yes and No. - /// - /// A message to show. - /// A title of the message box. - /// Returns true if user has selected YES. Otherwise returns false. - bool AskForConfirmation(string message, string title); + /// + /// Shows a message box to a user with buttons Yes and No. + /// + /// A message to show. + /// A title of the message box. + /// Returns true if user has selected YES. Otherwise returns false. + bool AskForConfirmation(string message, string title); - /// - /// Shows an information popup message to a user. - /// - /// A message content. - void ShowInformation(string message); + /// + /// Shows an information popup message to a user. + /// + /// A message content. + void ShowInformation(string message); - /// - /// Shows a warning popup message to a user. - /// - /// A message content. - void ShowWarning(string message); + /// + /// Shows a warning popup message to a user. + /// + /// A message content. + void ShowWarning(string message); - void ShowProductInBrowser(ProductSource productSource); - } + void ShowProductInBrowser(ProductSource? productSource); +} - [ExcludeFromCodeCoverage] - public class UserInteraction : IUserInteraction - { - private readonly IAgentQueryService _agentQuery; +[ExcludeFromCodeCoverage] +public class UserInteraction : IUserInteraction +{ + private readonly IAgentQueryService _agentQuery; - public UserInteraction(IAgentQueryService agentQuery) - { - _agentQuery = agentQuery; - } + public UserInteraction(IAgentQueryService agentQuery) + { + _agentQuery = agentQuery; + } - public bool AskForConfirmation(string message, string title) - { - var result = MessageBox.Show(message, title, MessageBoxButton.YesNo); - return result == MessageBoxResult.Yes; - } + public bool AskForConfirmation(string message, string title) + { + var result = MessageBox.Show(message, title, MessageBoxButton.YesNo); + return result == MessageBoxResult.Yes; + } - public void ShowInformation(string message) - { - MessageBox.Show(message, "Information", MessageBoxButton.OK, MessageBoxImage.Information); - } + public void ShowInformation(string message) + { + MessageBox.Show(message, "Information", MessageBoxButton.OK, MessageBoxImage.Information); + } - public void ShowWarning(string message) - { - MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning); - } + public void ShowWarning(string message) + { + MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning); + } - public void ShowProductInBrowser(ProductSource productSource) - { - if (productSource == null) - return; + public void ShowProductInBrowser(ProductSource? productSource) + { + if (productSource == null) + return; - var agentUrl = _agentQuery.FindByKey(productSource.AgentKey).Url; - var url = string.Format(agentUrl, productSource.AgentArgument); + var agentUrl = _agentQuery.FindByKey(productSource.AgentKey).Url; + var url = string.Format(agentUrl, productSource.AgentArgument); - url = url.Replace("&", "^&"); - Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); - } + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); } } diff --git a/PriceChecker.UI/NotifyIconResources.xaml.cs b/PriceChecker.UI/NotifyIconResources.xaml.cs index 57de972..0dfb11d 100644 --- a/PriceChecker.UI/NotifyIconResources.xaml.cs +++ b/PriceChecker.UI/NotifyIconResources.xaml.cs @@ -3,20 +3,19 @@ using Genius.PriceChecker.UI.ViewModels; using Hardcodet.Wpf.TaskbarNotification; -namespace Genius.PriceChecker.UI +namespace Genius.PriceChecker.UI; + +[ExcludeFromCodeCoverage] +public partial class NotifyIconResources : ResourceDictionary { - [ExcludeFromCodeCoverage] - public partial class NotifyIconResources : ResourceDictionary + public NotifyIconResources() { - public NotifyIconResources() - { - InitializeComponent(); + InitializeComponent(); - var notifyIcon = (TaskbarIcon)this["NotifyIcon"]; - var viewModel = (NotifyIconViewModel)notifyIcon.DataContext; + var notifyIcon = (TaskbarIcon)this["NotifyIcon"]; + var viewModel = (NotifyIconViewModel)notifyIcon.DataContext; - viewModel.ShowBalloonTipTriggered += (_, args) => - notifyIcon.ShowBalloonTip(args.Title, args.Message, args.Icon); - } + viewModel.ShowBalloonTipTriggered += (_, args) => + notifyIcon.ShowBalloonTip(args.Title, args.Message, args.Icon); } } diff --git a/PriceChecker.UI/PriceChecker.UI.csproj b/PriceChecker.UI/PriceChecker.UI.csproj index 729cf98..e46725d 100644 --- a/PriceChecker.UI/PriceChecker.UI.csproj +++ b/PriceChecker.UI/PriceChecker.UI.csproj @@ -2,7 +2,7 @@ WinExe - net5.0-windows + net6.0-windows true Genius.PriceChecker.UI PriceChecker @@ -41,18 +41,18 @@ - ..\..\atom\Atom.Infrastructure\bin\Debug\net5.0\Genius.Atom.Infrastructure.dll + ..\..\atom\Atom.Infrastructure\bin\Debug\net6.0\Genius.Atom.Infrastructure.dll - ..\..\atom\Atom.UI.Forms\bin\Debug\net5.0-windows\Genius.Atom.UI.Forms.dll + ..\..\atom\Atom.UI.Forms\bin\Debug\net6.0-windows\Genius.Atom.UI.Forms.dll - ..\..\atom\Atom.UI.Forms.Demo\bin\Debug\net5.0-windows\DotNetProjects.Input.Toolkit.dll + ..\..\atom\Atom.UI.Forms.Demo\bin\Debug\net6.0-windows\DotNetProjects.Input.Toolkit.dll - ..\..\atom\Atom.UI.Forms.Demo\bin\Debug\net5.0-windows\WpfAnimatedGif.dll + ..\..\atom\Atom.UI.Forms.Demo\bin\Debug\net6.0-windows\WpfAnimatedGif.dll diff --git a/PriceChecker.UI/Validation/MustBeUniqueValidationRule.cs b/PriceChecker.UI/Validation/MustBeUniqueValidationRule.cs index d7b8210..1705964 100644 --- a/PriceChecker.UI/Validation/MustBeUniqueValidationRule.cs +++ b/PriceChecker.UI/Validation/MustBeUniqueValidationRule.cs @@ -4,38 +4,39 @@ using System.Windows.Controls; using Genius.Atom.UI.Forms; -namespace Genius.PriceChecker.UI.Validation +namespace Genius.PriceChecker.UI.Validation; + +public class MustBeUniqueValidationRule : ValidationRule { - public class MustBeUniqueValidationRule : ValidationRule + private readonly ViewModelBase _viewModel; + private readonly string _currentCollectionPropertyName; + + public MustBeUniqueValidationRule(ViewModelBase viewModel, string uniqueCollectionPropertyName) { - private readonly ViewModelBase _viewModel; - private readonly string _currentCollectionPropertyName; + _viewModel = viewModel.NotNull(nameof(viewModel)); + _currentCollectionPropertyName = uniqueCollectionPropertyName.NotNull(nameof(viewModel)); + } - public MustBeUniqueValidationRule(ViewModelBase viewModel, string uniqueCollectionPropertyName) + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + if (value is not string valueString) { - _viewModel = viewModel; - _currentCollectionPropertyName = uniqueCollectionPropertyName; + return ValidationResult.ValidResult; } - public override ValidationResult Validate(object value, CultureInfo cultureInfo) - { - if (value is not string valueString) - { - return ValidationResult.ValidResult; - } + var uniqueCollection = (IEnumerable)_viewModel.GetType() + .GetProperty(_currentCollectionPropertyName) + .NotNull() + .GetValue(_viewModel) + .NotNull(); - var uniqueCollection = _viewModel.GetType() - .GetProperty(_currentCollectionPropertyName) - .GetValue(_viewModel) as IEnumerable; + var count = uniqueCollection.Count(x => x == valueString); - var count = uniqueCollection.Count(x => x == valueString); - - if (count > 1) - { - return new ValidationResult(false, "Value must be unique."); - } - - return ValidationResult.ValidResult; + if (count > 1) + { + return new ValidationResult(false, "Value must be unique."); } + + return ValidationResult.ValidResult; } } diff --git a/PriceChecker.UI/Validation/ValueCannotBeEmptyValidationRule.cs b/PriceChecker.UI/Validation/ValueCannotBeEmptyValidationRule.cs index dbff13f..cdd78a1 100644 --- a/PriceChecker.UI/Validation/ValueCannotBeEmptyValidationRule.cs +++ b/PriceChecker.UI/Validation/ValueCannotBeEmptyValidationRule.cs @@ -1,18 +1,17 @@ using System.Globalization; using System.Windows.Controls; -namespace Genius.PriceChecker.UI.Validation +namespace Genius.PriceChecker.UI.Validation; + +public class ValueCannotBeEmptyValidationRule : ValidationRule { - public class ValueCannotBeEmptyValidationRule : ValidationRule + public override ValidationResult Validate(object? value, CultureInfo cultureInfo) { - public override ValidationResult Validate(object value, CultureInfo cultureInfo) + if (value is string valueString && !string.IsNullOrWhiteSpace(valueString)) { - if (value is string valueString && !string.IsNullOrWhiteSpace(valueString)) - { - return ValidationResult.ValidResult; - } - - return new ValidationResult(false, "Value cannot be empty."); + return ValidationResult.ValidResult; } + + return new ValidationResult(false, "Value cannot be empty."); } } diff --git a/PriceChecker.UI/ValueConverters/DateTimeToHumanizedConverter.cs b/PriceChecker.UI/ValueConverters/DateTimeToHumanizedConverter.cs index 1d703eb..0b60f3b 100644 --- a/PriceChecker.UI/ValueConverters/DateTimeToHumanizedConverter.cs +++ b/PriceChecker.UI/ValueConverters/DateTimeToHumanizedConverter.cs @@ -1,24 +1,22 @@ -using System; using System.Globalization; using System.Windows.Data; using Humanizer; -namespace Genius.PriceChecker.UI.ValueConverters +namespace Genius.PriceChecker.UI.ValueConverters; + +public class DateTimeToHumanizedConverter : IValueConverter { - public class DateTimeToHumanizedConverter : IValueConverter + public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + if (value is not DateTime dt) { - if (value is not DateTime dt) - { - return null; - } - return dt.Humanize(false); + return null; } + return dt.Humanize(false); + } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); } -} \ No newline at end of file +} diff --git a/PriceChecker.UI/ViewModels/AgentViewModel.cs b/PriceChecker.UI/ViewModels/AgentViewModel.cs index fd6a2a3..0a49c21 100644 --- a/PriceChecker.UI/ViewModels/AgentViewModel.cs +++ b/PriceChecker.UI/ViewModels/AgentViewModel.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -6,111 +5,110 @@ using Genius.Atom.UI.Forms; using Genius.PriceChecker.UI.Validation; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +public interface IAgentViewModel : IViewModel, IHasDirtyFlag, ISelectable +{ + Agent GetOrCreateEntity(); + void ResetForm(); + + Guid? Id { get; } + string Key { get; } +} + +internal sealed class AgentViewModel : ViewModelBase, IAgentViewModel { - public interface IAgentViewModel : IViewModel, IHasDirtyFlag, ISelectable + private readonly IAgentsViewModel _owner; + private Agent? _agent; + + public AgentViewModel(IAgentsViewModel owner, Agent? agent) { - Agent GetOrCreateEntity(); - void ResetForm(); + _owner = owner; + _agent = agent; - Guid? Id { get; } - string Key { get; } + ResetForm(true); } - internal sealed class AgentViewModel : ViewModelBase, IAgentViewModel + public Agent GetOrCreateEntity() { - private readonly IAgentsViewModel _owner; - private Agent _agent; - - public AgentViewModel(IAgentsViewModel owner, Agent agent) - { - _owner = owner; - _agent = agent; + return _agent ??= new Agent { + Id = Guid.NewGuid(), + Key = Key, + Url = Url, + PricePattern = PricePattern, + DecimalDelimiter = DecimalDelimiter + }; + } - ResetForm(true); - } + public void ResetForm() + { + ResetForm(false); + } - public Agent GetOrCreateEntity() + private void ResetForm(bool firstTimeInit) + { + if (_agent is null && !firstTimeInit) { - return _agent ??= new Agent { - Id = Guid.NewGuid(), - Key = Key, - Url = Url, - PricePattern = PricePattern, - DecimalDelimiter = DecimalDelimiter - }; + return; } - public void ResetForm() - { - ResetForm(false); - } + if (firstTimeInit) + InitializeProperties(init); + else + init(); - private void ResetForm(bool firstTimeInit) + void init() { - if (_agent == null && !firstTimeInit) - { - return; - } - - if (firstTimeInit) - InitializeProperties(init); - else - init(); - - void init() - { - Key = _agent?.Key; - Url = _agent?.Url; - PricePattern = _agent?.PricePattern; - DecimalDelimiter = _agent?.DecimalDelimiter ?? '.'; - } + Key = _agent?.Key ?? string.Empty; + Url = _agent?.Url ?? string.Empty; + PricePattern = _agent?.PricePattern ?? string.Empty; + DecimalDelimiter = _agent?.DecimalDelimiter ?? '.'; } + } - [Browsable(false)] - public IEnumerable UsedKeys => _owner.Agents.Select(x => x.Key); + [Browsable(false)] + public IEnumerable UsedKeys => _owner.Agents.Select(x => x.Key); - [Browsable(false)] - public Guid? Id => _agent?.Id; + [Browsable(false)] + public Guid? Id => _agent?.Id; - public bool IsDirty - { - get => GetOrDefault(false); - set => RaiseAndSetIfChanged(value); - } + public bool IsDirty + { + get => GetOrDefault(false); + set => RaiseAndSetIfChanged(value); + } - [ValidationRule(typeof(ValueCannotBeEmptyValidationRule))] - [ValidationRule(typeof(MustBeUniqueValidationRule), nameof(UsedKeys))] - public string Key - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + [ValidationRule(typeof(ValueCannotBeEmptyValidationRule))] + [ValidationRule(typeof(MustBeUniqueValidationRule), nameof(UsedKeys))] + public string Key + { + get => GetOrDefault(string.Empty); + set => RaiseAndSetIfChanged(value); + } - [ValidationRule(typeof(ValueCannotBeEmptyValidationRule))] - public string Url - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + [ValidationRule(typeof(ValueCannotBeEmptyValidationRule))] + public string Url + { + get => GetOrDefault(string.Empty); + set => RaiseAndSetIfChanged(value); + } - [ValidationRule(typeof(ValueCannotBeEmptyValidationRule))] - public string PricePattern - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + [ValidationRule(typeof(ValueCannotBeEmptyValidationRule))] + public string PricePattern + { + get => GetOrDefault(string.Empty); + set => RaiseAndSetIfChanged(value); + } - public char DecimalDelimiter - { - get => GetOrDefault('.'); - set => RaiseAndSetIfChanged(value); - } + public char DecimalDelimiter + { + get => GetOrDefault('.'); + set => RaiseAndSetIfChanged(value); + } - public bool IsSelected - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + public bool IsSelected + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); } } diff --git a/PriceChecker.UI/ViewModels/AgentsViewModel.cs b/PriceChecker.UI/ViewModels/AgentsViewModel.cs index 624ef92..bf1d130 100644 --- a/PriceChecker.UI/ViewModels/AgentsViewModel.cs +++ b/PriceChecker.UI/ViewModels/AgentsViewModel.cs @@ -8,100 +8,99 @@ using Genius.PriceChecker.Core.Commands; using System.Threading.Tasks; -namespace Genius.PriceChecker.UI.ViewModels -{ - public interface IAgentsViewModel : ITabViewModel - { - ObservableCollection Agents { get; } - } - - internal sealed class AgentsViewModel : TabViewModelBase, IAgentsViewModel, IHasDirtyFlag - { - private readonly ICommandBus _commandBus; - private readonly IViewModelFactory _vmFactory; +namespace Genius.PriceChecker.UI.ViewModels; - public AgentsViewModel(IAgentQueryService agentQuery, IViewModelFactory vmFactory, - IUserInteraction ui, ICommandBus commandBus) - { - _commandBus = commandBus; - _vmFactory = vmFactory; +public interface IAgentsViewModel : ITabViewModel +{ + ObservableCollection Agents { get; } +} - var agentVms = agentQuery.GetAll().OrderBy(x => x.Key).Select(x => CreateAgentViewModel(x)); - Agents.ReplaceItems(agentVms); +internal sealed class AgentsViewModel : TabViewModelBase, IAgentsViewModel, IHasDirtyFlag +{ + private readonly ICommandBus _commandBus; + private readonly IViewModelFactory _vmFactory; - AddAgentCommand = new ActionCommand(_ => - { - Agents.Add(CreateAgentViewModel(null)); - IsDirty = true; - }); + public AgentsViewModel(IAgentQueryService agentQuery, IViewModelFactory vmFactory, + IUserInteraction ui, ICommandBus commandBus) + { + _commandBus = commandBus; + _vmFactory = vmFactory; - DeleteAgentCommand = new ActionCommand(async _ => - { - var selectedAgent = Agents.FirstOrDefault(x => x.IsSelected); - if (selectedAgent == null) - { - ui.ShowWarning("No agent selected."); - return; - } - if (!ui.AskForConfirmation($"Are you sure you want to delete the selected '{selectedAgent.Key}' agent?", "Delete agent")) - return; - - Agents.Remove(selectedAgent); - if (selectedAgent.Id.HasValue) - { - await commandBus.SendAsync(new AgentDeleteCommand(selectedAgent.Id.Value)); - } - }); - - CommitAgentsCommand = new ActionCommand(_ => CommitAgents(), - _ => IsDirty && !HasErrors); - - ResetChangesCommand = new ActionCommand(_ => { - foreach (var agent in Agents) - { - agent.ResetForm(); - } - IsDirty = false; - }, _ => IsDirty); - } + var agentVms = agentQuery.GetAll().OrderBy(x => x.Key).Select(x => CreateAgentViewModel(x)); + Agents.ReplaceItems(agentVms); - private IAgentViewModel CreateAgentViewModel(Agent x) + AddAgentCommand = new ActionCommand(_ => { - var agentVm = _vmFactory.CreateAgent(this, x); - agentVm.WhenChanged(x => x.IsDirty, x => IsDirty = IsDirty || x); - return agentVm; - } + Agents.Add(CreateAgentViewModel(null)); + IsDirty = true; + }); - private async Task CommitAgents() + DeleteAgentCommand = new ActionCommand(async _ => { - if (HasErrors) + var selectedAgent = Agents.FirstOrDefault(x => x.IsSelected); + if (selectedAgent == null) { + ui.ShowWarning("No agent selected."); return; } + if (!ui.AskForConfirmation($"Are you sure you want to delete the selected '{selectedAgent.Key}' agent?", "Delete agent")) + return; - var agents = Agents.Select(x => x.GetOrCreateEntity()).ToArray(); + Agents.Remove(selectedAgent); + if (selectedAgent.Id.HasValue) + { + await commandBus.SendAsync(new AgentDeleteCommand(selectedAgent.Id.Value)); + } + }); - await _commandBus.SendAsync(new AgentsStoreWithOverwriteCommand(agents)); + CommitAgentsCommand = new ActionCommand(_ => CommitAgents(), + _ => IsDirty && !HasErrors); + ResetChangesCommand = new ActionCommand(_ => { + foreach (var agent in Agents) + { + agent.ResetForm(); + } IsDirty = false; - } + }, _ => IsDirty); + } - public ObservableCollection Agents { get; } - = new TypedObservableList(); + private IAgentViewModel CreateAgentViewModel(Agent? x) + { + var agentVm = _vmFactory.CreateAgent(this, x); + agentVm.WhenChanged(x => x.IsDirty, x => IsDirty = IsDirty || x); + return agentVm; + } - public bool IsAddEditAgentVisible + private async Task CommitAgents() + { + if (HasErrors) { - get => GetOrDefault(false); - set => RaiseAndSetIfChanged(value); + return; } - public bool IsDirty { get; set; } + var agents = Agents.Select(x => x.GetOrCreateEntity()).ToArray(); - public override bool HasErrors => Agents.Any(x => x.HasErrors); + await _commandBus.SendAsync(new AgentsStoreWithOverwriteCommand(agents)); - public IActionCommand AddAgentCommand { get; } - public IActionCommand CommitAgentsCommand { get; } - public IActionCommand DeleteAgentCommand { get; } - public IActionCommand ResetChangesCommand { get; } + IsDirty = false; } -} \ No newline at end of file + + public ObservableCollection Agents { get; } + = new TypedObservableList(); + + public bool IsAddEditAgentVisible + { + get => GetOrDefault(false); + set => RaiseAndSetIfChanged(value); + } + + public bool IsDirty { get; set; } + + public override bool HasErrors => Agents.Any(x => x.HasErrors); + + public IActionCommand AddAgentCommand { get; } + public IActionCommand CommitAgentsCommand { get; } + public IActionCommand DeleteAgentCommand { get; } + public IActionCommand ResetChangesCommand { get; } +} diff --git a/PriceChecker.UI/ViewModels/LogItemViewModel.cs b/PriceChecker.UI/ViewModels/LogItemViewModel.cs index 04dcd8f..d054239 100644 --- a/PriceChecker.UI/ViewModels/LogItemViewModel.cs +++ b/PriceChecker.UI/ViewModels/LogItemViewModel.cs @@ -4,48 +4,47 @@ using Genius.Atom.UI.Forms; using Microsoft.Extensions.Logging; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +public interface ILogItemViewModel : IViewModel { - public interface ILogItemViewModel : IViewModel - { - LogLevel Severity { get; } - } + LogLevel Severity { get; } +} - internal sealed class LogItemViewModel : ViewModelBase, ILogItemViewModel +internal sealed class LogItemViewModel : ViewModelBase, ILogItemViewModel +{ + public LogItemViewModel() { - public LogItemViewModel() - { - CopyToClipboardCommand = new ActionCommand(_ => - Clipboard.SetText(Message)); - } + CopyToClipboardCommand = new ActionCommand(_ => + Clipboard.SetText(Message)); + } - [IconSource(nameof(SeverityIcon), 16d)] - public LogLevel Severity { get; set; } - public string Logger { get; set; } - public string Message { get; set; } + [IconSource(nameof(SeverityIcon), 16d)] + public LogLevel Severity { get; set; } + public string Logger { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; - [Browsable(false)] - public BitmapImage SeverityIcon + [Browsable(false)] + public BitmapImage? SeverityIcon + { + get { - get + var icon = Severity switch { - var icon = Severity switch - { - LogLevel.Warning => "Warning16", - LogLevel.Error => "Error16", - LogLevel.Critical => "Alert32", - {} => null - }; - if (icon == null) - return null; - return (BitmapImage)App.Current.FindResource(icon); - } + LogLevel.Warning => "Warning16", + LogLevel.Error => "Error16", + LogLevel.Critical => "Alert32", + {} => null + }; + if (icon is null) + return null; + return (BitmapImage)App.Current.FindResource(icon); } + } - [Browsable(false)] - public bool IsSeverityCritical => Severity == LogLevel.Critical; + [Browsable(false)] + public bool IsSeverityCritical => Severity == LogLevel.Critical; - [Icon("Copy16")] - public IActionCommand CopyToClipboardCommand { get; } - } + [Icon("Copy16")] + public IActionCommand CopyToClipboardCommand { get; } } diff --git a/PriceChecker.UI/ViewModels/LogsViewModel.cs b/PriceChecker.UI/ViewModels/LogsViewModel.cs index 8294e5d..16b3f70 100644 --- a/PriceChecker.UI/ViewModels/LogsViewModel.cs +++ b/PriceChecker.UI/ViewModels/LogsViewModel.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.ObjectModel; using System.Linq; using System.Windows; @@ -7,45 +6,44 @@ using Genius.Atom.UI.Forms; using Microsoft.Extensions.Logging; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +public interface ILogsViewModel : ITabViewModel +{ +} + +internal sealed class LogsViewModel : TabViewModelBase, ILogsViewModel { - public interface ILogsViewModel : ITabViewModel + public LogsViewModel(IEventBus eventBus) { + eventBus.WhenFired() + .Subscribe(x => { + Application.Current.Dispatcher.Invoke(() => + LogItems.Add(new LogItemViewModel { Severity = x.Severity, Logger = x.Logger, Message = x.Message }) + ); + }); + + CleanLogCommand = new ActionCommand(_ => LogItems.Clear()); + + LogItems.CollectionChanged += (_, args) => { + if (HasNewErrors) + return; + HasNewErrors = args.NewItems?.Cast() + .Any(x => x.Severity >= LogLevel.Error) ?? false; + }; + + Activated.Executed.Subscribe(_ => HasNewErrors = false); + Deactivated.Executed.Subscribe(_ => HasNewErrors = false); } - internal sealed class LogsViewModel : TabViewModelBase, ILogsViewModel + public ObservableCollection LogItems { get; } + = new TypedObservableList(); + + public bool HasNewErrors { - public LogsViewModel(IEventBus eventBus) - { - eventBus.WhenFired() - .Subscribe(x => { - Application.Current.Dispatcher.Invoke(() => - LogItems.Add(new LogItemViewModel { Severity = x.Severity, Logger = x.Logger, Message = x.Message }) - ); - }); - - CleanLogCommand = new ActionCommand(_ => LogItems.Clear()); - - LogItems.CollectionChanged += (_, args) => { - if (HasNewErrors) - return; - HasNewErrors = args.NewItems?.Cast() - .Any(x => x.Severity >= LogLevel.Error) ?? false; - }; - - Activated.Executed.Subscribe(_ => HasNewErrors = false); - Deactivated.Executed.Subscribe(_ => HasNewErrors = false); - } - - public ObservableCollection LogItems { get; } - = new TypedObservableList(); - - public bool HasNewErrors - { - get => GetOrDefault(false); - set => RaiseAndSetIfChanged(value); - } - - public IActionCommand CleanLogCommand { get; } + get => GetOrDefault(false); + set => RaiseAndSetIfChanged(value); } + + public IActionCommand CleanLogCommand { get; } } diff --git a/PriceChecker.UI/ViewModels/MainViewModel.cs b/PriceChecker.UI/ViewModels/MainViewModel.cs index 0a3f374..9d92a0e 100644 --- a/PriceChecker.UI/ViewModels/MainViewModel.cs +++ b/PriceChecker.UI/ViewModels/MainViewModel.cs @@ -1,96 +1,94 @@ -using System; using System.Collections.Generic; using System.Windows.Shell; using Genius.Atom.UI.Forms; using Genius.PriceChecker.UI.Helpers; using Hardcodet.Wpf.TaskbarNotification; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +public interface IMainViewModel : IViewModel { - public interface IMainViewModel : IViewModel - { - } +} - internal sealed class MainViewModel : ViewModelBase, IMainViewModel +internal sealed class MainViewModel : ViewModelBase, IMainViewModel +{ + private readonly ITrackerScanContext _scanContext; + private readonly INotifyIconViewModel _notifyViewModel; + + public MainViewModel( + ITrackerViewModel tracker, + IAgentsViewModel agents, + ISettingsViewModel settings, + ILogsViewModel logs, + ITrackerScanContext scanContext, + INotifyIconViewModel notifyViewModel) { - private readonly ITrackerScanContext _scanContext; - private readonly INotifyIconViewModel _notifyViewModel; + _scanContext = scanContext; + _notifyViewModel = notifyViewModel; - public MainViewModel( - ITrackerViewModel tracker, - IAgentsViewModel agents, - ISettingsViewModel settings, - ILogsViewModel logs, - ITrackerScanContext scanContext, - INotifyIconViewModel notifyViewModel) - { - _scanContext = scanContext; - _notifyViewModel = notifyViewModel; + Tabs = new() { + tracker, + agents, + settings, + logs + }; - Tabs = new() { - tracker, - agents, - settings, - logs - }; + scanContext.ScanProgress.Subscribe(args => UpdateProgress(args.Status, args.Progress)); + } - scanContext.ScanProgress.Subscribe(args => UpdateProgress(args.Status, args.Progress)); + private void UpdateProgress(TrackerScanStatus status, double progress) + { + if (status == Helpers.TrackerScanStatus.InProgress) + { + ProgressState = TaskbarItemProgressState.Normal; + ProgressValue = progress; } - - private void UpdateProgress(TrackerScanStatus status, double progress) + else if (status == Helpers.TrackerScanStatus.InProgressWithErrors) { - if (status == Helpers.TrackerScanStatus.InProgress) - { - ProgressState = TaskbarItemProgressState.Normal; - ProgressValue = progress; - } - else if (status == Helpers.TrackerScanStatus.InProgressWithErrors) - { - ProgressState = TaskbarItemProgressState.Paused; - ProgressValue = progress; - } - else if (status == Helpers.TrackerScanStatus.Finished) - { - var message = _scanContext.HasNewLowestPrice ? - "Prices for some products have become even lower! Check it out." : - "Nothing interesting has been caught."; - if (_scanContext.HasErrors) - { - message += Environment.NewLine + "NOTE: Some products could not finish scanning properly. Check the logs for details."; - } - _notifyViewModel.ShowBalloonTip("Scan finished", message, - _scanContext.HasErrors ? BalloonIcon.Warning : BalloonIcon.Info); - ProgressState = TaskbarItemProgressState.None; - ProgressValue = 0; - } - else + ProgressState = TaskbarItemProgressState.Paused; + ProgressValue = progress; + } + else if (status == Helpers.TrackerScanStatus.Finished) + { + var message = _scanContext.HasNewLowestPrice ? + "Prices for some products have become even lower! Check it out." : + "Nothing interesting has been caught."; + if (_scanContext.HasErrors) { - ProgressState = TaskbarItemProgressState.None; - ProgressValue = 0; + message += Environment.NewLine + "NOTE: Some products could not finish scanning properly. Check the logs for details."; } + _notifyViewModel.ShowBalloonTip("Scan finished", message, + _scanContext.HasErrors ? BalloonIcon.Warning : BalloonIcon.Info); + ProgressState = TaskbarItemProgressState.None; + ProgressValue = 0; } - - public List Tabs { get; } - - public int SelectedTabIndex + else { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value, (@old, @new) => { - Tabs[@old].Deactivated.Execute(null); - Tabs[@new].Activated.Execute(null); - }); + ProgressState = TaskbarItemProgressState.None; + ProgressValue = 0; } + } - public TaskbarItemProgressState ProgressState - { - get => GetOrDefault(TaskbarItemProgressState.None); - set => RaiseAndSetIfChanged(value); - } + public List Tabs { get; } - public double ProgressValue - { - get => GetOrDefault(0d); - set => RaiseAndSetIfChanged(value); - } + public int SelectedTabIndex + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value, (@old, @new) => { + Tabs[@old].Deactivated.Execute(null); + Tabs[@new].Activated.Execute(null); + }); + } + + public TaskbarItemProgressState ProgressState + { + get => GetOrDefault(TaskbarItemProgressState.None); + set => RaiseAndSetIfChanged(value); + } + + public double ProgressValue + { + get => GetOrDefault(0d); + set => RaiseAndSetIfChanged(value); } } diff --git a/PriceChecker.UI/ViewModels/NotifyIconViewModel.cs b/PriceChecker.UI/ViewModels/NotifyIconViewModel.cs index 542f4e7..025f86c 100644 --- a/PriceChecker.UI/ViewModels/NotifyIconViewModel.cs +++ b/PriceChecker.UI/ViewModels/NotifyIconViewModel.cs @@ -1,40 +1,35 @@ -using System; -using System.Windows; +using System.Windows; using System.Windows.Input; using Genius.Atom.UI.Forms; -using Genius.PriceChecker.UI.Helpers; using Hardcodet.Wpf.TaskbarNotification; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +public interface INotifyIconViewModel { - public interface INotifyIconViewModel - { - void ShowBalloonTip(string title, string message, BalloonIcon icon); - } + void ShowBalloonTip(string title, string message, BalloonIcon icon); +} + +internal sealed class NotifyIconViewModel : INotifyIconViewModel +{ + public readonly record struct ShowBalloonTipEventArgs(string Title, string Message, BalloonIcon Icon); - internal sealed class NotifyIconViewModel : INotifyIconViewModel + internal event EventHandler ShowBalloonTipTriggered = (_, __) => {}; + + public void ShowBalloonTip(string title, string message, BalloonIcon icon) { - internal event EventHandler ShowBalloonTipTriggered; + ShowBalloonTipTriggered.Invoke(this, new ShowBalloonTipEventArgs(title, message, icon)); + } - public void ShowBalloonTip(string title, string message, BalloonIcon icon) + public ICommand ShowWindowCommand => new ActionCommand(_ => { - ShowBalloonTipTriggered.Invoke(this, new ShowBalloonTipEventArgs { - Title = title, - Message = message, - Icon = icon - }); - } - - public ICommand ShowWindowCommand => new ActionCommand(_ => - { - Application.Current.MainWindow.Show(); - Application.Current.MainWindow.Focus(); - }); - - public ICommand HideWindowCommand => new ActionCommand(_ => - Application.Current.MainWindow.Hide()); - - public ICommand ExitApplicationCommand => new ActionCommand(_ => - Application.Current.Shutdown()); - } + Application.Current.MainWindow.Show(); + Application.Current.MainWindow.Focus(); + }); + + public ICommand HideWindowCommand => new ActionCommand(_ => + Application.Current.MainWindow.Hide()); + + public ICommand ExitApplicationCommand => new ActionCommand(_ => + Application.Current.Shutdown()); } diff --git a/PriceChecker.UI/ViewModels/SettingsViewModel.cs b/PriceChecker.UI/ViewModels/SettingsViewModel.cs index 40f6b0d..cd9f46f 100644 --- a/PriceChecker.UI/ViewModels/SettingsViewModel.cs +++ b/PriceChecker.UI/ViewModels/SettingsViewModel.cs @@ -2,62 +2,61 @@ using Genius.PriceChecker.Core.Repositories; using Genius.Atom.UI.Forms; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +public interface ISettingsViewModel : ITabViewModel { - public interface ISettingsViewModel : ITabViewModel - { - } +} - internal sealed class SettingsViewModel : TabViewModelBase, ISettingsViewModel +internal sealed class SettingsViewModel : TabViewModelBase, ISettingsViewModel +{ + public SettingsViewModel(ISettingsRepository repo) { - public SettingsViewModel(ISettingsRepository repo) - { - var settings = repo.Get(); + var settings = repo.Get(); - AutoRefreshMinuteOptions = new [] { + AutoRefreshMinuteOptions = new [] { #if DEBUG - new AutoRefreshOption { Name = "1 minute (DEBUG ONLY)", Value = 1 }, + new AutoRefreshOption { Name = "1 minute (DEBUG ONLY)", Value = 1 }, #endif - new AutoRefreshOption { Name = "1 hour", Value = 60 }, - new AutoRefreshOption { Name = "3 hours", Value = 180 }, - new AutoRefreshOption { Name = "8 hours", Value = 480 }, - new AutoRefreshOption { Name = "1 day", Value = 1440 } - }; - - AutoRefreshEnabled = settings.AutoRefreshEnabled; - AutoRefreshMinutes = AutoRefreshMinuteOptions.FirstOrDefault(x => x.Value == settings.AutoRefreshMinutes) - ?? AutoRefreshMinuteOptions[0]; - - this.PropertyChanged += (sender, args) => { - settings.AutoRefreshEnabled = AutoRefreshEnabled; - settings.AutoRefreshMinutes = AutoRefreshMinutes.Value; - repo.Store(settings); - }; - } - - public bool AutoRefreshEnabled - { - get => GetOrDefault(false); - set => RaiseAndSetIfChanged(value); - } - - public AutoRefreshOption[] AutoRefreshMinuteOptions - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } - - public AutoRefreshOption AutoRefreshMinutes - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } - - - public class AutoRefreshOption - { - public string Name { get; init; } - public int Value { get; init; } - } + new AutoRefreshOption { Name = "1 hour", Value = 60 }, + new AutoRefreshOption { Name = "3 hours", Value = 180 }, + new AutoRefreshOption { Name = "8 hours", Value = 480 }, + new AutoRefreshOption { Name = "1 day", Value = 1440 } + }; + + AutoRefreshEnabled = settings.AutoRefreshEnabled; + AutoRefreshMinutes = AutoRefreshMinuteOptions.FirstOrDefault(x => x.Value == settings.AutoRefreshMinutes) + ?? AutoRefreshMinuteOptions[0]; + + this.PropertyChanged += (sender, args) => { + settings.AutoRefreshEnabled = AutoRefreshEnabled; + settings.AutoRefreshMinutes = AutoRefreshMinutes.Value; + repo.Store(settings); + }; + } + + public bool AutoRefreshEnabled + { + get => GetOrDefault(false); + set => RaiseAndSetIfChanged(value); + } + + public AutoRefreshOption[] AutoRefreshMinuteOptions + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } + + public AutoRefreshOption AutoRefreshMinutes + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } + + + public class AutoRefreshOption + { + public string Name { get; init; } + public int Value { get; init; } } } diff --git a/PriceChecker.UI/ViewModels/TrackerProductSourceViewModel.cs b/PriceChecker.UI/ViewModels/TrackerProductSourceViewModel.cs index 0323273..64b53ae 100644 --- a/PriceChecker.UI/ViewModels/TrackerProductSourceViewModel.cs +++ b/PriceChecker.UI/ViewModels/TrackerProductSourceViewModel.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Windows; @@ -6,53 +5,52 @@ using Genius.Atom.UI.Forms; using Genius.PriceChecker.UI.Helpers; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +internal sealed class TrackerProductSourceViewModel : ViewModelBase { - internal sealed class TrackerProductSourceViewModel : ViewModelBase + public TrackerProductSourceViewModel(IUserInteraction ui, ProductSource? productSource, decimal? lastPrice) { - public TrackerProductSourceViewModel(IUserInteraction ui, ProductSource productSource, decimal? lastPrice) - { - InitializeProperties(() => { - Id = productSource?.Id ?? Guid.NewGuid(); - AgentKey = productSource?.AgentKey; - Argument = productSource?.AgentArgument; - LastPrice = lastPrice; - }); - - ShowInBrowserCommand = new ActionCommand(_ => - ui.ShowProductInBrowser(productSource)); - } - - [Browsable(false)] - public Guid Id { get; set; } - - [SelectFromList(nameof(TrackerProductViewModel.Agents), fromOwnerContext: true)] - public string AgentKey - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } - - public string Argument - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } - - [ReadOnly(true)] - [DisplayFormat(DataFormatString = "€ #,##0.00")] - [Style(HorizontalAlignment = HorizontalAlignment.Right)] - public decimal? LastPrice - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } - - [Icon("Trash16")] - public IActionCommand DeleteCommand { get; } = new ActionCommand(); - - [Browsable(true)] - [Icon("Web16")] - public IActionCommand ShowInBrowserCommand { get; } + InitializeProperties(() => { + Id = productSource?.Id ?? Guid.NewGuid(); + AgentKey = productSource?.AgentKey ?? string.Empty; + Argument = productSource?.AgentArgument ?? string.Empty; + LastPrice = lastPrice; + }); + + ShowInBrowserCommand = new ActionCommand(_ => + ui.ShowProductInBrowser(productSource)); } + + [Browsable(false)] + public Guid Id { get; set; } + + [SelectFromList(nameof(TrackerProductViewModel.Agents), fromOwnerContext: true)] + public string AgentKey + { + get => GetOrDefault(string.Empty); + set => RaiseAndSetIfChanged(value); + } + + public string Argument + { + get => GetOrDefault(string.Empty); + set => RaiseAndSetIfChanged(value); + } + + [ReadOnly(true)] + [DisplayFormat(DataFormatString = "€ #,##0.00")] + [Style(HorizontalAlignment = HorizontalAlignment.Right)] + public decimal? LastPrice + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } + + [Icon("Trash16")] + public IActionCommand DeleteCommand { get; } = new ActionCommand(); + + [Browsable(true)] + [Icon("Web16")] + public IActionCommand ShowInBrowserCommand { get; } } diff --git a/PriceChecker.UI/ViewModels/TrackerProductViewModel.cs b/PriceChecker.UI/ViewModels/TrackerProductViewModel.cs index c008fdb..0462d93 100644 --- a/PriceChecker.UI/ViewModels/TrackerProductViewModel.cs +++ b/PriceChecker.UI/ViewModels/TrackerProductViewModel.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; @@ -20,303 +19,306 @@ using Genius.PriceChecker.UI.ValueConverters; using ReactiveUI; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +public interface ITrackerProductViewModel : IViewModel, ISelectable { - public interface ITrackerProductViewModel : IViewModel, ISelectable - { - void Reconcile(bool lowestPriceUpdated); - void SetFailed(string errorMessage); - - Guid Id { get; } - string Name { get; } - ProductScanStatus Status { get; set; } - IActionCommand CommitProductCommand { get; } - IActionCommand RefreshPriceCommand { get; } - } + void Reconcile(bool lowestPriceUpdated); + void SetFailed(string errorMessage); + + Guid Id { get; } + string Name { get; } + ProductScanStatus Status { get; set; } + IActionCommand CommitProductCommand { get; } + IActionCommand RefreshPriceCommand { get; } +} - [ShowOnlyBrowsable(true)] - internal sealed class TrackerProductViewModel : ViewModelBase, ITrackerProductViewModel +[ShowOnlyBrowsable(true)] +internal sealed class TrackerProductViewModel : ViewModelBase, ITrackerProductViewModel +{ + private readonly IAgentQueryService _agentQuery; + private readonly IProductQueryService _productQuery; + private readonly IProductStatusProvider _statusProvider; + private readonly ICommandBus _commandBus; + private readonly IUserInteraction _ui; + + private Product? _product; + + public TrackerProductViewModel(Product? product, IEventBus eventBus, + ICommandBus commandBus, + IAgentQueryService agentQuery, + IProductQueryService productQuery, IProductStatusProvider statusProvider, + IUserInteraction ui) { - private readonly IAgentQueryService _agentQuery; - private readonly IProductQueryService _productQuery; - private readonly IProductStatusProvider _statusProvider; - private readonly ICommandBus _commandBus; - private readonly IUserInteraction _ui; - - private Product _product; - - public TrackerProductViewModel(Product product, IEventBus eventBus, - ICommandBus commandBus, - IAgentQueryService agentQuery, - IProductQueryService productQuery, IProductStatusProvider statusProvider, - IUserInteraction ui) + _agentQuery = agentQuery; + _productQuery = productQuery; + _statusProvider = statusProvider; + _commandBus = commandBus; + _product = product; + _ui = ui; + + InitializeProperties(() => { - _agentQuery = agentQuery; - _productQuery = productQuery; - _statusProvider = statusProvider; - _commandBus = commandBus; - _product = product; - _ui = ui; - - InitializeProperties(() => + RefreshAgents(); + RefreshCategories(); + + if (_product != null) { - RefreshAgents(); - RefreshCategories(); + ResetForm(); + Reconcile(false); + } + }); - if (_product != null) - { - ResetForm(); - Reconcile(false); - } - }); + CommitProductCommand = new ActionCommand(_ => CommitProduct()); - CommitProductCommand = new ActionCommand(_ => CommitProduct()); + ShowInBrowserCommand = new ActionCommand(_ => + ui.ShowProductInBrowser(_product?.Lowest?.ProductSource)); - ShowInBrowserCommand = new ActionCommand(_ => - ui.ShowProductInBrowser(_product.Lowest?.ProductSource)); + AddSourceCommand = new ActionCommand(_ => + Sources.Add(CreateSourceViewModel(null))); - AddSourceCommand = new ActionCommand(_ => - Sources.Add(CreateSourceViewModel(null))); + ResetCommand = new ActionCommand(_ => ResetForm(), _ => _product is not null); - ResetCommand = new ActionCommand(_ => ResetForm(), _ => _product != null); + DropPricesCommand = new ActionCommand(async _ => + { + if (!_ui.AskForConfirmation("Are you sure?", "Prices drop confirmation")) + return; + await commandBus.SendAsync(new ProductDropPricesCommand(_product!.Id)); + Reconcile(true); + }, _ => _product is not null); - DropPricesCommand = new ActionCommand(async _ => - { - if (!_ui.AskForConfirmation("Are you sure?", "Prices drop confirmation")) - return; - await commandBus.SendAsync(new ProductDropPricesCommand(_product.Id)); - Reconcile(true); - }); + RefreshPriceCommand = new ActionCommand(async _ => + { + if (Status == ProductScanStatus.Scanning) + return; + await commandBus.SendAsync(new ProductEnqueueScanCommand(product.Id)); + }, _ => Status != ProductScanStatus.Scanning); + + eventBus.WhenFired() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => + RefreshAgents() + ); + eventBus.WhenFired() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => + RefreshCategories() + ); + } - RefreshPriceCommand = new ActionCommand(async _ => - { - if (Status == ProductScanStatus.Scanning) - return; - await commandBus.SendAsync(new ProductEnqueueScanCommand(product.Id)); - }, _ => Status != ProductScanStatus.Scanning); - - eventBus.WhenFired() - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => - RefreshAgents() - ); - eventBus.WhenFired() - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => - RefreshCategories() - ); + public void Reconcile(bool lowestPriceUpdated) + { + Product product = _product.NotNull(); + var previousLowestPrice = LowestPrice; + LowestPrice = product.Lowest?.Price; + LowestFoundOn = product.Lowest?.FoundDate; + RecentPrice = product.Recent.Any() + ? product.Recent.Min(x => x.Price) + : null; + Status = lowestPriceUpdated && product.Lowest != null ? + ProductScanStatus.ScannedNewLowest : + _statusProvider.DetermineStatus(product); + LastUpdated = null; + + if (lowestPriceUpdated && LowestPrice.HasValue && previousLowestPrice.HasValue) + { + var x = (1 - LowestPrice.Value / previousLowestPrice) * 100; + StatusText = $"The new price is by {x:0}% less than by previous scan ({LowestPrice.Value:#,##0.00} vs {previousLowestPrice.Value:#,##0.00})"; } - - public void Reconcile(bool lowestPriceUpdated) + else if (Status == ProductScanStatus.ScannedWithErrors) { - var previousLowestPrice = LowestPrice; - LowestPrice = _product.Lowest?.Price; - LowestFoundOn = _product.Lowest?.FoundDate; - RecentPrice = _product.Recent.Any() - ? _product.Recent.Min(x => x.Price) - : null; - Status = lowestPriceUpdated && _product.Lowest != null ? - ProductScanStatus.ScannedNewLowest : - _statusProvider.DetermineStatus(_product); - LastUpdated = null; - - if (lowestPriceUpdated && LowestPrice.HasValue && previousLowestPrice.HasValue) - { - var x = (1 - LowestPrice.Value / previousLowestPrice) * 100; - StatusText = $"The new price is by {x:0}% less than by previous scan ({LowestPrice.Value:#,##0.00} vs {previousLowestPrice.Value:#,##0.00})"; - } - else if (Status == ProductScanStatus.ScannedWithErrors) - { - StatusText = "One or more source hasn't updated its price. Check the logs."; - } - else - { - StatusText = string.Empty; - } - - if (_product.Recent.Any()) - { - LastUpdated = _product.Recent.Max(x => x.FoundDate); - - var pricesDict = _product.Recent.ToDictionary(x => x.ProductSourceId, x => x.Price); - foreach (var source in Sources) - { - source.LastPrice = pricesDict.ContainsKey(source.Id) ? - pricesDict[source.Id] : - null; - } - } - else - { - foreach (var source in Sources) - { - source.LastPrice = null; - } - } + StatusText = "One or more source hasn't updated its price. Check the logs."; } - - public void SetFailed(string errorMessage) + else { - Status = ProductScanStatus.Failed; - StatusText = errorMessage; + StatusText = string.Empty; } - private async Task CommitProduct() + if (product.Recent.Any()) { - if (string.IsNullOrEmpty(Name)) - { - _ui.ShowWarning("Product name cannot be empty."); - return; - } - - var sources = Sources.Select(x => new ProductSource - { - Id = x.Id, - AgentKey = x.AgentKey, - AgentArgument = x.Argument - }).ToArray(); + LastUpdated = product.Recent.Max(x => x.FoundDate); - if (_product == null) + var pricesDict = product.Recent.ToDictionary(x => x.ProductSourceId, x => x.Price); + foreach (var source in Sources) { - var productId = await _commandBus.SendAsync(new ProductCreateCommand(Name, Category, Description, sources)); - _product = _productQuery.FindById(productId); + source.LastPrice = pricesDict.ContainsKey(source.Id) ? + pricesDict[source.Id] : + null; } - else + } + else + { + foreach (var source in Sources) { - await _commandBus.SendAsync(new ProductUpdateCommand(_product.Id, Name, Category, Description, sources)); + source.LastPrice = null; } } + } - private TrackerProductSourceViewModel CreateSourceViewModel(ProductSource productSource) + public void SetFailed(string errorMessage) + { + Status = ProductScanStatus.Failed; + StatusText = errorMessage; + } + + private async Task CommitProduct() + { + if (string.IsNullOrEmpty(Name)) { - var lastPrice = productSource == null || _product?.Recent == null ? - null : - _product.Recent.FirstOrDefault(x => x.ProductSourceId == productSource.Id)?.Price; - var vm = new TrackerProductSourceViewModel(_ui, productSource, lastPrice); - vm.DeleteCommand.Executed.Subscribe(_ => - Sources.Remove(vm)); - return vm; + _ui.ShowWarning("Product name cannot be empty."); + return; } - private void RefreshAgents() + var sources = Sources.Select(x => new ProductSource { - Agents = _agentQuery.GetAll().Select(x => x.Key).ToList(); - } + Id = x.Id, + AgentKey = x.AgentKey, + AgentArgument = x.Argument + }).ToArray(); - private void RefreshCategories() + if (_product is null) { - Categories.ReplaceItems( - _productQuery.GetAll().Select(x => x.Category).Distinct()); + var productId = await _commandBus.SendAsync(new ProductCreateCommand(Name, Category, Description, sources)); + _product = _productQuery.FindById(productId); } - - private void ResetForm() + else { - Name = _product.Name; - Category = _product.Category; - Description = _product.Description; - - var productSourceVms = _product.Sources.Select(x => CreateSourceViewModel(x)); - Sources.ReplaceItems(productSourceVms); + await _commandBus.SendAsync(new ProductUpdateCommand(_product.Id, Name, Category, Description, sources)); } + } - public Guid Id => _product.Id; + private TrackerProductSourceViewModel CreateSourceViewModel(ProductSource? productSource) + { + var lastPrice = productSource is null || _product?.Recent is null + ? null + : _product.Recent.FirstOrDefault(x => x.ProductSourceId == productSource.Id)?.Price; + var vm = new TrackerProductSourceViewModel(_ui, productSource, lastPrice); + vm.DeleteCommand.Executed.Subscribe(_ => + Sources.Remove(vm)); + return vm; + } - public IReadOnlyCollection Agents { get; private set; } + private void RefreshAgents() + { + Agents = _agentQuery.GetAll().Select(x => x.Key).ToList(); + } - public ObservableCollection Sources { get; } = new ObservableCollection(); - public ObservableCollection Categories { get; } = new ObservableCollection(); + private void RefreshCategories() + { + Categories.ReplaceItems( + _productQuery.GetAll() + .Where(x => x.Category is not null) + .Select(x => x.Category!).Distinct()); + } - [Browsable(true)] - [IconSource(nameof(StatusIcon), fixedSize : 16d, hideText : true)] - [TooltipSource(nameof(StatusText))] - [Style(HorizontalAlignment = HorizontalAlignment.Right)] - public ProductScanStatus Status - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value, (_, __) => - OnPropertyChanged(nameof(StatusIcon))); - } + private void ResetForm() + { + Name = _product?.Name ?? Name; + Category = _product?.Category; + Description = _product?.Description; - public BitmapImage StatusIcon => ResourcesHelper.GetStatusIcon(Status); + var productSourceVms = _product?.Sources.Select(x => CreateSourceViewModel(x)) + ?? new List(); + Sources.ReplaceItems(productSourceVms); + } - public string StatusText - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + public Guid Id => _product.NotNull().Id; - [Browsable(true)] - [FilterBy] - public string Name - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + public IReadOnlyCollection Agents { get; private set; } = new List(); - [GroupBy] - public string Category - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + public ObservableCollection Sources { get; } = new(); + public ObservableCollection Categories { get; } = new(); - public string Description - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + [Browsable(true)] + [IconSource(nameof(StatusIcon), fixedSize : 16d, hideText : true)] + [TooltipSource(nameof(StatusText))] + [Style(HorizontalAlignment = HorizontalAlignment.Right)] + public ProductScanStatus Status + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value, (_, __) => + OnPropertyChanged(nameof(StatusIcon))); + } - [Browsable(true)] - [DisplayFormat(DataFormatString = "€ #,##0.00")] - [Style(HorizontalAlignment = HorizontalAlignment.Right)] - public decimal? LowestPrice - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + public BitmapImage? StatusIcon => ResourcesHelper.GetStatusIcon(Status); - [Browsable(true)] - [DisplayFormat(DataFormatString = "€ #,##0.00")] - [Style(HorizontalAlignment = HorizontalAlignment.Right)] - public decimal? RecentPrice - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + public string StatusText + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } - [Browsable(true)] - [ValueConverter(typeof(DateTimeToHumanizedConverter))] - public DateTime? LowestFoundOn - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + [Browsable(true)] + [FilterBy] + public string Name + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } - [Browsable(true)] - [ValueConverter(typeof(DateTimeToHumanizedConverter))] - public DateTime? LastUpdated - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + [GroupBy] + public string? Category + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } - public bool IsSelected - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + public string? Description + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } + + [Browsable(true)] + [DisplayFormat(DataFormatString = "€ #,##0.00")] + [Style(HorizontalAlignment = HorizontalAlignment.Right)] + public decimal? LowestPrice + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } + + [Browsable(true)] + [DisplayFormat(DataFormatString = "€ #,##0.00")] + [Style(HorizontalAlignment = HorizontalAlignment.Right)] + public decimal? RecentPrice + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } - public IActionCommand CommitProductCommand { get; } + [Browsable(true)] + [ValueConverter(typeof(DateTimeToHumanizedConverter))] + public DateTime? LowestFoundOn + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } - [Browsable(true)] - [Icon("Web16")] - public IActionCommand ShowInBrowserCommand { get; } - public IActionCommand AddSourceCommand { get; } - public IActionCommand ResetCommand { get; } - public IActionCommand DropPricesCommand { get; } + [Browsable(true)] + [ValueConverter(typeof(DateTimeToHumanizedConverter))] + public DateTime? LastUpdated + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } - [Browsable(true)] - [Icon("Refresh16")] - public IActionCommand RefreshPriceCommand { get; } + public bool IsSelected + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); } + + public IActionCommand CommitProductCommand { get; } + + [Browsable(true)] + [Icon("Web16")] + public IActionCommand ShowInBrowserCommand { get; } + public IActionCommand AddSourceCommand { get; } + public IActionCommand ResetCommand { get; } + public IActionCommand DropPricesCommand { get; } + + [Browsable(true)] + [Icon("Refresh16")] + public IActionCommand RefreshPriceCommand { get; } } diff --git a/PriceChecker.UI/ViewModels/TrackerViewModel.cs b/PriceChecker.UI/ViewModels/TrackerViewModel.cs index 41b2fb8..593ea98 100644 --- a/PriceChecker.UI/ViewModels/TrackerViewModel.cs +++ b/PriceChecker.UI/ViewModels/TrackerViewModel.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -13,148 +12,147 @@ using Genius.Atom.Infrastructure.Commands; using Genius.PriceChecker.Core.Commands; -namespace Genius.PriceChecker.UI.ViewModels -{ - public interface ITrackerViewModel : ITabViewModel - { } +namespace Genius.PriceChecker.UI.ViewModels; + +public interface ITrackerViewModel : ITabViewModel +{ } - internal sealed class TrackerViewModel : TabViewModelBase, ITrackerViewModel +internal sealed class TrackerViewModel : TabViewModelBase, ITrackerViewModel +{ + private readonly IEventBus _eventBus; + private readonly IProductQueryService _productQuery; + private readonly IViewModelFactory _vmFactory; + private readonly ITrackerScanContext _scanContext; + + public TrackerViewModel(IEventBus eventBus, + IProductQueryService productQuery, + IViewModelFactory vmFactory, + IUserInteraction ui, + ITrackerScanContext scanContext, + ICommandBus commandBus) { - private readonly IEventBus _eventBus; - private readonly IProductQueryService _productQuery; - private readonly IViewModelFactory _vmFactory; - private readonly ITrackerScanContext _scanContext; - - public TrackerViewModel(IEventBus eventBus, - IProductQueryService productQuery, - IViewModelFactory vmFactory, - IUserInteraction ui, - ITrackerScanContext scanContext, - ICommandBus commandBus) - { - _eventBus = eventBus; - _productQuery = productQuery; - _vmFactory = vmFactory; - _scanContext = scanContext; - - RefreshAllCommand = new ActionCommand(_ => { - IsAddEditProductVisible = false; - EnqueueScan(Products); - }); - RefreshSelectedCommand = new ActionCommand(_ => { - IsAddEditProductVisible = false; - EnqueueScan(Products.Where(x => x.IsSelected).ToArray()); - }); - OpenAddProductFlyoutCommand = new ActionCommand(_ => { - IsAddEditProductVisible = !IsAddEditProductVisible; - if (IsAddEditProductVisible) - { - EditingProduct = vmFactory.CreateTrackerProduct(null); - EditingProduct.CommitProductCommand.Executed - .Take(1) - .Subscribe(_ => { - IsAddEditProductVisible = false; - ReloadList(); - }); - } - }); - OpenEditProductFlyoutCommand = new ActionCommand(_ => { - EditingProduct = Products.FirstOrDefault(x => x.IsSelected); - EditingProduct?.CommitProductCommand.Executed - .Take(1) - .Subscribe(_ => IsAddEditProductVisible = false); - IsAddEditProductVisible = EditingProduct != null; - }); - - DeleteProductCommand = new ActionCommand(async _ => { - IsAddEditProductVisible = false; - var selectedProduct = Products.FirstOrDefault(x => x.IsSelected); - if (selectedProduct == null) - { - ui.ShowWarning("No product selected."); - return; - } - if (!ui.AskForConfirmation($"Are you sure you want to delete the selected '{selectedProduct.Name}' product?", "Delete product")) - return; - - Products.Remove(selectedProduct); - await commandBus.SendAsync(new ProductDeleteCommand(selectedProduct.Id)); - }); - - _eventBus.WhenFired() - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(ev => - Products.First(x => x.Id == ev.Product.Id).Status = Core.Models.ProductScanStatus.Scanning - ); - _eventBus.WhenFired() - .Subscribe(ev => - Products.First(x => x.Id == ev.Product.Id).Reconcile(ev.LowestPriceUpdated)); - _eventBus.WhenFired() - .Subscribe(ev => - Products.First(x => x.Id == ev.Product.Id).SetFailed(ev.ErrorMessage)); - - Deactivated.Executed.Subscribe(_ => - IsAddEditProductVisible = false); - - RefreshOptions = new List { - new DropDownMenuItem("Refresh all", RefreshAllCommand), - new DropDownMenuItem("Refresh selected", RefreshSelectedCommand), - }; - - ReloadList(); - } + _eventBus = eventBus; + _productQuery = productQuery; + _vmFactory = vmFactory; + _scanContext = scanContext; - private void EnqueueScan(ICollection products) - { - _scanContext.NotifyStarted(products.Count); - foreach (var product in products) + RefreshAllCommand = new ActionCommand(_ => { + IsAddEditProductVisible = false; + EnqueueScan(Products); + }); + RefreshSelectedCommand = new ActionCommand(_ => { + IsAddEditProductVisible = false; + EnqueueScan(Products.Where(x => x.IsSelected).ToArray()); + }); + OpenAddProductFlyoutCommand = new ActionCommand(_ => { + IsAddEditProductVisible = !IsAddEditProductVisible; + if (IsAddEditProductVisible) { - product.RefreshPriceCommand.Execute(null); + EditingProduct = vmFactory.CreateTrackerProduct(null); + EditingProduct.CommitProductCommand.Executed + .Take(1) + .Subscribe(_ => { + IsAddEditProductVisible = false; + ReloadList(); + }); } - } - - private void ReloadList() - { + }); + OpenEditProductFlyoutCommand = new ActionCommand(_ => { + EditingProduct = Products.FirstOrDefault(x => x.IsSelected); + EditingProduct?.CommitProductCommand.Executed + .Take(1) + .Subscribe(_ => IsAddEditProductVisible = false); + IsAddEditProductVisible = EditingProduct != null; + }); + + DeleteProductCommand = new ActionCommand(async _ => { IsAddEditProductVisible = false; - var productVms = _productQuery.GetAll() - .Select(x => _vmFactory.CreateTrackerProduct(x)) - .ToList(); - foreach (var productVm in productVms) + var selectedProduct = Products.FirstOrDefault(x => x.IsSelected); + if (selectedProduct == null) { - productVm.WhenChanged(x => x.Status, status => - _scanContext.NotifyProgressChange(status)); + ui.ShowWarning("No product selected."); + return; } - Products.ReplaceItems(productVms); - } - - public List RefreshOptions { get; } - - public ObservableCollection Products { get; } - = new TypedObservableList(); + if (!ui.AskForConfirmation($"Are you sure you want to delete the selected '{selectedProduct.Name}' product?", "Delete product")) + return; + + Products.Remove(selectedProduct); + await commandBus.SendAsync(new ProductDeleteCommand(selectedProduct.Id)); + }); + + _eventBus.WhenFired() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(ev => + Products.First(x => x.Id == ev.Product.Id).Status = Core.Models.ProductScanStatus.Scanning + ); + _eventBus.WhenFired() + .Subscribe(ev => + Products.First(x => x.Id == ev.Product.Id).Reconcile(ev.LowestPriceUpdated)); + _eventBus.WhenFired() + .Subscribe(ev => + Products.First(x => x.Id == ev.Product.Id).SetFailed(ev.ErrorMessage)); + + Deactivated.Executed.Subscribe(_ => + IsAddEditProductVisible = false); + + RefreshOptions = new List { + new DropDownMenuItem("Refresh all", RefreshAllCommand), + new DropDownMenuItem("Refresh selected", RefreshSelectedCommand), + }; + + ReloadList(); + } - [FilterContext] - public string Filter + private void EnqueueScan(ICollection products) + { + _scanContext.NotifyStarted(products.Count); + foreach (var product in products) { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); + product.RefreshPriceCommand.Execute(null); } + } - public bool IsAddEditProductVisible + private void ReloadList() + { + IsAddEditProductVisible = false; + var productVms = _productQuery.GetAll() + .Select(x => _vmFactory.CreateTrackerProduct(x)) + .ToList(); + foreach (var productVm in productVms) { - get => GetOrDefault(false); - set => RaiseAndSetIfChanged(value); + productVm.WhenChanged(x => x.Status, status => + _scanContext.NotifyProgressChange(status)); } + Products.ReplaceItems(productVms); + } - public ITrackerProductViewModel EditingProduct - { - get => GetOrDefault(); - set => RaiseAndSetIfChanged(value); - } + public List RefreshOptions { get; } + + public ObservableCollection Products { get; } + = new TypedObservableList(); - public ICommand RefreshAllCommand { get; } - public ICommand RefreshSelectedCommand { get; } - public ICommand OpenAddProductFlyoutCommand { get; } - public ICommand OpenEditProductFlyoutCommand { get; } - public ICommand DeleteProductCommand { get; } + [FilterContext] + public string Filter + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } + + public bool IsAddEditProductVisible + { + get => GetOrDefault(false); + set => RaiseAndSetIfChanged(value); } + + public ITrackerProductViewModel? EditingProduct + { + get => GetOrDefault(); + set => RaiseAndSetIfChanged(value); + } + + public ICommand RefreshAllCommand { get; } + public ICommand RefreshSelectedCommand { get; } + public ICommand OpenAddProductFlyoutCommand { get; } + public ICommand OpenEditProductFlyoutCommand { get; } + public ICommand DeleteProductCommand { get; } } diff --git a/PriceChecker.UI/ViewModels/ViewModelFactory.cs b/PriceChecker.UI/ViewModels/ViewModelFactory.cs index 1ac126e..a63fa2a 100644 --- a/PriceChecker.UI/ViewModels/ViewModelFactory.cs +++ b/PriceChecker.UI/ViewModels/ViewModelFactory.cs @@ -1,55 +1,51 @@ using System.Diagnostics.CodeAnalysis; +using Genius.Atom.Infrastructure.Events; +using Genius.Atom.Infrastructure.Commands; using Genius.PriceChecker.Core.Models; using Genius.PriceChecker.Core.Repositories; using Genius.PriceChecker.Core.Services; -using Genius.Atom.Infrastructure.Events; using Genius.PriceChecker.UI.Helpers; -using Genius.Atom.Infrastructure.Commands; -namespace Genius.PriceChecker.UI.ViewModels +namespace Genius.PriceChecker.UI.ViewModels; + +public interface IViewModelFactory { - public interface IViewModelFactory + IAgentViewModel CreateAgent(IAgentsViewModel owner, Agent? agent); + ITrackerProductViewModel CreateTrackerProduct(Product? product); +} + +[ExcludeFromCodeCoverage] +internal sealed class ViewModelFactory : IViewModelFactory +{ + private readonly IAgentQueryService _agentQuery; + private readonly IProductQueryService _productQuery; + private readonly IProductStatusProvider _statusProvider; + private readonly IEventBus _eventBus; + private readonly ICommandBus _commandBus; + private readonly IUserInteraction _ui; + + public ViewModelFactory(IEventBus eventBus, + ICommandBus commandBus, + IAgentQueryService agentQuery, IProductQueryService productQuery, + IProductStatusProvider statusProvider, + IUserInteraction ui) { - IAgentViewModel CreateAgent(IAgentsViewModel owner, Agent agent); - ITrackerProductViewModel CreateTrackerProduct(Product product); + _eventBus = eventBus; + _commandBus = commandBus; + _agentQuery = agentQuery; + _productQuery = productQuery; + _statusProvider = statusProvider; + _ui = ui; } - [ExcludeFromCodeCoverage] - internal sealed class ViewModelFactory : IViewModelFactory + public IAgentViewModel CreateAgent(IAgentsViewModel owner, Agent? agent) { - private readonly IAgentQueryService _agentQuery; - private readonly IProductQueryService _productQuery; - private readonly IProductPriceManager _productPriceMng; - private readonly IProductStatusProvider _statusProvider; - private readonly IEventBus _eventBus; - private readonly ICommandBus _commandBus; - private readonly IUserInteraction _ui; - - public ViewModelFactory(IEventBus eventBus, - ICommandBus commandBus, - IAgentQueryService agentQuery, IProductQueryService productQuery, - IProductPriceManager productPriceMng, - IProductStatusProvider statusProvider, - IUserInteraction ui) - { - _eventBus = eventBus; - _commandBus = commandBus; - _agentQuery = agentQuery; - _productQuery = productQuery; - _productPriceMng = productPriceMng; - _statusProvider = statusProvider; - _ui = ui; - } - - public IAgentViewModel CreateAgent(IAgentsViewModel owner, Agent agent) - { - return new AgentViewModel(owner, agent); - } + return new AgentViewModel(owner, agent); + } - public ITrackerProductViewModel CreateTrackerProduct(Product product) - { - return new TrackerProductViewModel(product, _eventBus, _commandBus, _agentQuery, - _productQuery, _statusProvider, _ui); - } + public ITrackerProductViewModel CreateTrackerProduct(Product? product) + { + return new TrackerProductViewModel(product, _eventBus, _commandBus, _agentQuery, + _productQuery, _statusProvider, _ui); } } diff --git a/PriceChecker.UI/Views/AddEditProductFlyout.xaml b/PriceChecker.UI/Views/AddEditProductFlyout.xaml index beefd6f..ea370b8 100644 --- a/PriceChecker.UI/Views/AddEditProductFlyout.xaml +++ b/PriceChecker.UI/Views/AddEditProductFlyout.xaml @@ -9,7 +9,7 @@ mc:Ignorable="d" Header="Add/Edit product" Position="Right" - Width="500"> + Width="600"> diff --git a/PriceChecker.UI/Views/AddEditProductFlyout.xaml.cs b/PriceChecker.UI/Views/AddEditProductFlyout.xaml.cs index 3ac8742..7d9b2a9 100644 --- a/PriceChecker.UI/Views/AddEditProductFlyout.xaml.cs +++ b/PriceChecker.UI/Views/AddEditProductFlyout.xaml.cs @@ -1,13 +1,12 @@ using System.Diagnostics.CodeAnalysis; -namespace Genius.PriceChecker.UI.Views +namespace Genius.PriceChecker.UI.Views; + +[ExcludeFromCodeCoverage] +public partial class AddEditProductFlyout { - [ExcludeFromCodeCoverage] - public partial class AddEditProductFlyout + public AddEditProductFlyout() { - public AddEditProductFlyout() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/PriceChecker.UI/Views/Agents.xaml.cs b/PriceChecker.UI/Views/Agents.xaml.cs index 5349223..c380537 100644 --- a/PriceChecker.UI/Views/Agents.xaml.cs +++ b/PriceChecker.UI/Views/Agents.xaml.cs @@ -1,13 +1,12 @@ using System.Diagnostics.CodeAnalysis; -namespace Genius.PriceChecker.UI.Views +namespace Genius.PriceChecker.UI.Views; + +[ExcludeFromCodeCoverage] +public partial class Agents { - [ExcludeFromCodeCoverage] - public partial class Agents + public Agents() { - public Agents() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/PriceChecker.UI/Views/Logs.xaml.cs b/PriceChecker.UI/Views/Logs.xaml.cs index e71f294..034252b 100644 --- a/PriceChecker.UI/Views/Logs.xaml.cs +++ b/PriceChecker.UI/Views/Logs.xaml.cs @@ -1,13 +1,12 @@ using System.Diagnostics.CodeAnalysis; -namespace Genius.PriceChecker.UI.Views +namespace Genius.PriceChecker.UI.Views; + +[ExcludeFromCodeCoverage] +public partial class Logs { - [ExcludeFromCodeCoverage] - public partial class Logs + public Logs() { - public Logs() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/PriceChecker.UI/Views/MainWindow.xaml.cs b/PriceChecker.UI/Views/MainWindow.xaml.cs index d3ea768..2f2eea7 100644 --- a/PriceChecker.UI/Views/MainWindow.xaml.cs +++ b/PriceChecker.UI/Views/MainWindow.xaml.cs @@ -2,16 +2,15 @@ using Genius.PriceChecker.UI.ViewModels; using MahApps.Metro.Controls; -namespace Genius.PriceChecker.UI.Views +namespace Genius.PriceChecker.UI.Views; + +[ExcludeFromCodeCoverage] +public partial class MainWindow : MetroWindow { - [ExcludeFromCodeCoverage] - public partial class MainWindow : MetroWindow + public MainWindow(IMainViewModel mainVm) { - public MainWindow(IMainViewModel mainVm) - { - InitializeComponent(); + InitializeComponent(); - DataContext = mainVm; - } + DataContext = mainVm; } } diff --git a/PriceChecker.UI/Views/Settings.xaml.cs b/PriceChecker.UI/Views/Settings.xaml.cs index 805baa5..4fa90b0 100644 --- a/PriceChecker.UI/Views/Settings.xaml.cs +++ b/PriceChecker.UI/Views/Settings.xaml.cs @@ -1,13 +1,12 @@ using System.Diagnostics.CodeAnalysis; -namespace Genius.PriceChecker.UI.Views +namespace Genius.PriceChecker.UI.Views; + +[ExcludeFromCodeCoverage] +public partial class Settings { - [ExcludeFromCodeCoverage] - public partial class Settings + public Settings() { - public Settings() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/PriceChecker.UI/Views/Tracker.xaml.cs b/PriceChecker.UI/Views/Tracker.xaml.cs index 18a1350..decba03 100644 --- a/PriceChecker.UI/Views/Tracker.xaml.cs +++ b/PriceChecker.UI/Views/Tracker.xaml.cs @@ -5,33 +5,32 @@ using Genius.Atom.UI.Forms; using Genius.PriceChecker.UI.ViewModels; -namespace Genius.PriceChecker.UI.Views +namespace Genius.PriceChecker.UI.Views; + +[ExcludeFromCodeCoverage] +public partial class Tracker { - [ExcludeFromCodeCoverage] - public partial class Tracker + public Tracker() { - public Tracker() - { - InitializeComponent(); + InitializeComponent(); - this.Loaded += (sender, args) => - WpfHelpers.AddFlyout(this, nameof(TrackerViewModel.IsAddEditProductVisible), nameof(TrackerViewModel.EditingProduct)); - } + this.Loaded += (sender, args) => + WpfHelpers.AddFlyout(this, nameof(TrackerViewModel.IsAddEditProductVisible), nameof(TrackerViewModel.EditingProduct)); + } - private void Filter_KeyUp(object sender, KeyEventArgs e) - { - var filterTextbox = (TextBox)sender; + private void Filter_KeyUp(object sender, KeyEventArgs e) + { + var filterTextbox = (TextBox)sender; - if (e.Key == Key.Enter || e.Key == Key.Escape) + if (e.Key == Key.Enter || e.Key == Key.Escape) + { + if (e.Key == Key.Escape) { - if (e.Key == Key.Escape) - { - filterTextbox.Text = string.Empty; - } - - var bindingExpr = BindingOperations.GetBindingExpression(filterTextbox, TextBox.TextProperty); - bindingExpr?.UpdateSource(); + filterTextbox.Text = string.Empty; } + + var bindingExpr = BindingOperations.GetBindingExpression(filterTextbox, TextBox.TextProperty); + bindingExpr?.UpdateSource(); } } }