From 711cc11805a4e34111f5a511ec7b23a148f0fa98 Mon Sep 17 00:00:00 2001 From: Alberto Balbo Date: Wed, 29 Apr 2026 16:09:25 +0200 Subject: [PATCH] Aggiunti unit test e integrazione CI per AutoBidder - Creato progetto `Tests/AutoBidder.Tests` con test su servizi, modelli e utility principali - Aggiornato `.dockerignore` per escludere test e risultati - Aggiornati workflow GitHub Actions per .NET 10, salvataggio e upload dei risultati test come artifact - Aggiornata soluzione e csproj per includere/escludere i test correttamente - I test coprono casi standard ed edge case, senza modifiche funzionali al codice di produzione --- Mimante/.dockerignore | 3 +- Mimante/.gitea/workflows/deploy.yml | 15 +- Mimante/.github/workflows/ci-cd.yml | 13 +- Mimante/AutoBidder.csproj | 19 +- Mimante/AutoBidder.sln | 29 +- .../ApplicationStateServiceTests.cs | 285 ++++++++++++++++ ...ctionMonitorProductValueExtensionsTests.cs | 148 +++++++++ .../AuctionStateServiceTests.cs | 155 +++++++++ .../AuctionStatisticsTests.cs | 192 +++++++++++ .../AutoBidder.Tests/AutoBidder.Tests.csproj | 20 ++ .../BidStrategyServiceTests.cs | 303 +++++++++++++++++ .../ClosedAuctionsScraperTests.cs | 187 +++++++++++ .../AutoBidder.Tests/HtmlCacheServiceTests.cs | 77 +++++ Mimante/Tests/AutoBidder.Tests/ModelTests.cs | 314 ++++++++++++++++++ .../AutoBidder.Tests/ProductInsightsTests.cs | 161 +++++++++ .../ProductStatisticsServiceTests.cs | 188 +++++++++++ .../ProductValueCalculatorTests.cs | 285 ++++++++++++++++ 17 files changed, 2379 insertions(+), 15 deletions(-) create mode 100644 Mimante/Tests/AutoBidder.Tests/ApplicationStateServiceTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/AuctionMonitorProductValueExtensionsTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/AuctionStateServiceTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/AuctionStatisticsTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/AutoBidder.Tests.csproj create mode 100644 Mimante/Tests/AutoBidder.Tests/BidStrategyServiceTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/ClosedAuctionsScraperTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/HtmlCacheServiceTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/ModelTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/ProductInsightsTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/ProductStatisticsServiceTests.cs create mode 100644 Mimante/Tests/AutoBidder.Tests/ProductValueCalculatorTests.cs diff --git a/Mimante/.dockerignore b/Mimante/.dockerignore index 5521130..51ddf6e 100644 --- a/Mimante/.dockerignore +++ b/Mimante/.dockerignore @@ -42,7 +42,8 @@ bld/ **/packages/* !**/packages/build/ -# Test results +# Test project and results +Tests/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* diff --git a/Mimante/.gitea/workflows/deploy.yml b/Mimante/.gitea/workflows/deploy.yml index b3c93d6..4fb6ddc 100644 --- a/Mimante/.gitea/workflows/deploy.yml +++ b/Mimante/.gitea/workflows/deploy.yml @@ -11,8 +11,8 @@ on: workflow_dispatch: # Permette trigger manuale env: - DOTNET_VERSION: '8.0.x' - REGISTRY: ${{ secrets.GITEA_REGISTRY }} +DOTNET_VERSION: '10.0.x' +REGISTRY: ${{ secrets.GITEA_REGISTRY }} jobs: # Job 1: Build e Test .NET @@ -45,8 +45,15 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Run tests - run: dotnet test --no-restore --verbosity normal --logger "console;verbosity=detailed" - continue-on-error: true + run: dotnet test --no-restore --verbosity normal --logger "console;verbosity=detailed" --results-directory ./test-results --logger "trx;LogFileName=results.trx" + + - name: Upload test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results + path: ./test-results + retention-days: 30 - name: Publish artifacts run: dotnet publish --configuration Release --no-build --output ./publish diff --git a/Mimante/.github/workflows/ci-cd.yml b/Mimante/.github/workflows/ci-cd.yml index d2160fb..d107f41 100644 --- a/Mimante/.github/workflows/ci-cd.yml +++ b/Mimante/.github/workflows/ci-cd.yml @@ -20,7 +20,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore @@ -29,8 +29,15 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Test - run: dotnet test --no-restore --verbosity normal - continue-on-error: true + run: dotnet test --no-restore --verbosity normal --results-directory ./test-results --logger "trx;LogFileName=results.trx" + + - name: Upload test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results + path: ./test-results + retention-days: 30 - name: Publish run: dotnet publish --configuration Release --no-build --output ./publish diff --git a/Mimante/AutoBidder.csproj b/Mimante/AutoBidder.csproj index 1073314..c84855e 100644 --- a/Mimante/AutoBidder.csproj +++ b/Mimante/AutoBidder.csproj @@ -25,6 +25,12 @@ + + + + + + @@ -84,6 +90,7 @@ + @@ -100,14 +107,14 @@ - + - - + + @@ -136,15 +143,15 @@ - + - - + + diff --git a/Mimante/AutoBidder.sln b/Mimante/AutoBidder.sln index 32f1060..d133a8f 100644 --- a/Mimante/AutoBidder.sln +++ b/Mimante/AutoBidder.sln @@ -1,10 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11217.181 d18.0 +VisualStudioVersion = 18.0.11217.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder", "AutoBidder.csproj", "{9BBAEF93-DF66-432C-9349-459E272D6538}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder.Tests", "Tests\AutoBidder.Tests\AutoBidder.Tests.csproj", "{A54E2BBB-8921-428F-A9B4-C042ABD1B584}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,10 +43,33 @@ Global {9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.Build.0 = Release|Any CPU {9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.ActiveCfg = Release|Any CPU {9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.Build.0 = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM.ActiveCfg = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM.Build.0 = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM64.Build.0 = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x64.ActiveCfg = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x64.Build.0 = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x86.ActiveCfg = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x86.Build.0 = Debug|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|Any CPU.Build.0 = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM.ActiveCfg = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM.Build.0 = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM64.ActiveCfg = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM64.Build.0 = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x64.ActiveCfg = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x64.Build.0 = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x86.ActiveCfg = Release|Any CPU + {A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A54E2BBB-8921-428F-A9B4-C042ABD1B584} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1C55CA56-D270-4D9A-91DA-410BF131E905} EndGlobalSection diff --git a/Mimante/Tests/AutoBidder.Tests/ApplicationStateServiceTests.cs b/Mimante/Tests/AutoBidder.Tests/ApplicationStateServiceTests.cs new file mode 100644 index 0000000..3a5ed9e --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/ApplicationStateServiceTests.cs @@ -0,0 +1,285 @@ +using AutoBidder.Models; +using AutoBidder.Services; +using Xunit; + +namespace AutoBidder.Tests; + +public class ApplicationStateServiceTests +{ + // === Auctions === + + [Fact] + public void Auctions_Initially_Empty() + { + var service = new ApplicationStateService(); + Assert.Empty(service.Auctions); + } + + [Fact] + public void AddAuction_AddsToList() + { + var service = new ApplicationStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "1", Name = "Test" }); + + Assert.Single(service.Auctions); + Assert.Equal("1", service.Auctions[0].AuctionId); + } + + [Fact] + public void AddAuction_DuplicateId_DoesNotAdd() + { + var service = new ApplicationStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "1", Name = "First" }); + service.AddAuction(new AuctionInfo { AuctionId = "1", Name = "Second" }); + + Assert.Single(service.Auctions); + Assert.Equal("First", service.Auctions[0].Name); + } + + [Fact] + public void RemoveAuction_ExistingAuction_RemovesFromList() + { + var service = new ApplicationStateService(); + var auction = new AuctionInfo { AuctionId = "1" }; + service.AddAuction(auction); + + service.RemoveAuction(auction); + + Assert.Empty(service.Auctions); + } + + [Fact] + public void RemoveAuction_SelectedAuction_ClearsSelection() + { + var service = new ApplicationStateService(); + var auction = new AuctionInfo { AuctionId = "1" }; + service.AddAuction(auction); + service.SetSelectedAuctionDirect(auction); + + service.RemoveAuction(auction); + + Assert.Null(service.SelectedAuction); + } + + [Fact] + public void UpdateAuction_ExistingAuction_ReplacesInList() + { + var service = new ApplicationStateService(); + var original = new AuctionInfo { AuctionId = "1", Name = "Original" }; + service.AddAuction(original); + + var updated = new AuctionInfo { AuctionId = "1", Name = "Updated" }; + service.UpdateAuction(updated); + + Assert.Single(service.Auctions); + Assert.Equal("Updated", service.Auctions[0].Name); + } + + [Fact] + public void UpdateAuction_SelectedAuction_UpdatesSelection() + { + var service = new ApplicationStateService(); + var auction = new AuctionInfo { AuctionId = "1", Name = "Original" }; + service.AddAuction(auction); + service.SetSelectedAuctionDirect(auction); + + var updated = new AuctionInfo { AuctionId = "1", Name = "Updated" }; + service.UpdateAuction(updated); + + Assert.Equal("Updated", service.SelectedAuction!.Name); + } + + [Fact] + public void SetAuctions_ReplacesEntireList() + { + var service = new ApplicationStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "old" }); + + var newList = new List + { + new() { AuctionId = "new1" }, + new() { AuctionId = "new2" } + }; + service.SetAuctions(newList); + + Assert.Equal(2, service.Auctions.Count); + } + + [Fact] + public void GetAuctionById_ExistingId_ReturnsAuction() + { + var service = new ApplicationStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "42", Name = "Found" }); + + var result = service.GetAuctionById("42"); + + Assert.NotNull(result); + Assert.Equal("Found", result!.Name); + } + + [Fact] + public void GetAuctionById_NonExistent_ReturnsNull() + { + var service = new ApplicationStateService(); + Assert.Null(service.GetAuctionById("missing")); + } + + // === SelectedAuction === + + [Fact] + public void SetSelectedAuctionDirect_SetsWithoutEvent() + { + var service = new ApplicationStateService(); + var auction = new AuctionInfo { AuctionId = "1" }; + + service.SetSelectedAuctionDirect(auction); + + Assert.Equal("1", service.SelectedAuction!.AuctionId); + } + + // === Log === + + [Fact] + public void AddLog_AddsEntry() + { + var service = new ApplicationStateService(); + service.AddLog("Test message"); + + Assert.Single(service.GlobalLog); + Assert.Equal("Test message", service.GlobalLog[0].Message); + } + + [Fact] + public void AddLog_TruncatesAtLimit() + { + var service = new ApplicationStateService(); + for (int i = 0; i < 600; i++) + { + service.AddLog($"Message {i}"); + } + + Assert.True(service.GlobalLog.Count <= 500); + } + + [Fact] + public void ClearLog_EmptiesLog() + { + var service = new ApplicationStateService(); + service.AddLog("Test"); + service.AddLog("Test2"); + + service.ClearLog(); + + Assert.Empty(service.GlobalLog); + } + + // === Manual Bidding === + + [Fact] + public void IsManualBidding_Default_ReturnsFalse() + { + var service = new ApplicationStateService(); + Assert.False(service.IsManualBidding("123")); + } + + [Fact] + public void StartManualBidding_SetsFlag() + { + var service = new ApplicationStateService(); + service.StartManualBidding("123"); + + Assert.True(service.IsManualBidding("123")); + } + + [Fact] + public void StopManualBidding_ClearsFlag() + { + var service = new ApplicationStateService(); + service.StartManualBidding("123"); + service.StopManualBidding("123"); + + Assert.False(service.IsManualBidding("123")); + } + + // === Monitoring State === + + [Fact] + public void IsMonitoringActive_Default_False() + { + var service = new ApplicationStateService(); + Assert.False(service.IsMonitoringActive); + } + + [Fact] + public void IsMonitoringActive_SetTrue_ReturnsTrue() + { + var service = new ApplicationStateService(); + service.IsMonitoringActive = true; + Assert.True(service.IsMonitoringActive); + } + + // === Browser State === + + [Fact] + public void BrowserCategoryIndex_DefaultZero() + { + var service = new ApplicationStateService(); + Assert.Equal(0, service.BrowserCategoryIndex); + } + + [Fact] + public void BrowserSearchQuery_DefaultEmpty() + { + var service = new ApplicationStateService(); + Assert.Equal("", service.BrowserSearchQuery); + } + + [Fact] + public void BrowserCategoryIndex_SetAndGet() + { + var service = new ApplicationStateService(); + service.BrowserCategoryIndex = 5; + Assert.Equal(5, service.BrowserCategoryIndex); + } + + [Fact] + public void BrowserSearchQuery_SetAndGet() + { + var service = new ApplicationStateService(); + service.BrowserSearchQuery = "iphone"; + Assert.Equal("iphone", service.BrowserSearchQuery); + } + + // === Direct References === + + [Fact] + public void GetAuctionsDirectRef_ReturnsSameList() + { + var service = new ApplicationStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "1" }); + + var direct = service.GetAuctionsDirectRef(); + + Assert.Single(direct); + } + + [Fact] + public void GetLogDirectRef_ReturnsSameList() + { + var service = new ApplicationStateService(); + service.AddLog("test"); + + var direct = service.GetLogDirectRef(); + + Assert.Single(direct); + } + + // === SessionStart === + + [Fact] + public void SessionStart_ReturnsNonDefault() + { + var service = new ApplicationStateService(); + Assert.True(service.SessionStart > DateTime.MinValue); + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/AuctionMonitorProductValueExtensionsTests.cs b/Mimante/Tests/AutoBidder.Tests/AuctionMonitorProductValueExtensionsTests.cs new file mode 100644 index 0000000..7393fa0 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/AuctionMonitorProductValueExtensionsTests.cs @@ -0,0 +1,148 @@ +using AutoBidder.Models; +using AutoBidder.Services; +using Xunit; + +namespace AutoBidder.Tests; + +public class AuctionMonitorProductValueExtensionsTests +{ + // === IsStillWorthBidding === + + [Fact] + public void IsStillWorthBidding_NoCalculatedValue_ReturnsTrue() + { + var auction = new AuctionInfo { AuctionId = "1", CalculatedValue = null }; + + Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction)); + } + + [Fact] + public void IsStillWorthBidding_NoBuyNowPrice_ReturnsTrue() + { + var auction = new AuctionInfo + { + AuctionId = "1", + CalculatedValue = new ProductValue { BuyNowPrice = null } + }; + + Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction)); + } + + [Fact] + public void IsStillWorthBidding_NoSavings_ReturnsTrue() + { + var auction = new AuctionInfo + { + AuctionId = "1", + CalculatedValue = new ProductValue { BuyNowPrice = 100.0, Savings = null } + }; + + Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction)); + } + + [Fact] + public void IsStillWorthBidding_PositiveSavings_ReturnsTrue() + { + var auction = new AuctionInfo + { + AuctionId = "1", + CalculatedValue = new ProductValue + { + BuyNowPrice = 100.0, + Savings = 50.0, + SavingsPercentage = 50.0 + } + }; + + Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction, minSavingsPercentage: 10)); + } + + [Fact] + public void IsStillWorthBidding_NegativeSavingsBelowThreshold_ReturnsFalse() + { + var auction = new AuctionInfo + { + AuctionId = "1", + CalculatedValue = new ProductValue + { + BuyNowPrice = 100.0, + Savings = -20.0, + SavingsPercentage = -20.0 + } + }; + + Assert.False(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction, minSavingsPercentage: 0)); + } + + [Fact] + public void IsStillWorthBidding_SavingsExactlyAtThreshold_ReturnsTrue() + { + var auction = new AuctionInfo + { + AuctionId = "1", + CalculatedValue = new ProductValue + { + BuyNowPrice = 100.0, + Savings = 10.0, + SavingsPercentage = 10.0 + } + }; + + Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction, minSavingsPercentage: 10.0)); + } + + // === UpdateProductValue === + + [Fact] + public void UpdateProductValue_SetsCalculatedValue() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = 50.0, + ShippingCost = 5.0, + BidsUsedOnThisAuction = 10 + }; + var state = new AuctionState { Price = 2.0 }; + + AuctionMonitorProductValueExtensions.UpdateProductValue(auction, state); + + Assert.NotNull(auction.CalculatedValue); + Assert.Equal(2.0, auction.CalculatedValue!.CurrentPrice); + } + + [Fact] + public void UpdateProductValue_TotalBidsBasedOnPrice() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BidsUsedOnThisAuction = 0 + }; + var state = new AuctionState { Price = 1.50 }; // 150 total bids + + AuctionMonitorProductValueExtensions.UpdateProductValue(auction, state); + + Assert.Equal(150, auction.CalculatedValue!.TotalBids); + } + + [Fact] + public void UpdateProductValue_WithLogValue_DoesNotThrow() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = 100.0, + BidsUsedOnThisAuction = 5 + }; + var state = new AuctionState { Price = 1.0 }; + + // Should not throw + AuctionMonitorProductValueExtensions.UpdateProductValue(auction, state, logValue: true); + + Assert.NotNull(auction.CalculatedValue); + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/AuctionStateServiceTests.cs b/Mimante/Tests/AutoBidder.Tests/AuctionStateServiceTests.cs new file mode 100644 index 0000000..90025e7 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/AuctionStateServiceTests.cs @@ -0,0 +1,155 @@ +using AutoBidder.Models; +using AutoBidder.Services; +using Xunit; + +namespace AutoBidder.Tests; + +public class AuctionStateServiceTests +{ + // === GetAuction === + + [Fact] + public void GetAuction_NonExistentId_ReturnsNull() + { + var service = new AuctionStateService(); + Assert.Null(service.GetAuction("non-existent")); + } + + // === AddAuction === + + [Fact] + public void AddAuction_NewAuction_CanBeRetrieved() + { + var service = new AuctionStateService(); + var auction = new AuctionInfo { AuctionId = "123", Name = "Test" }; + + service.AddAuction(auction); + + var retrieved = service.GetAuction("123"); + Assert.NotNull(retrieved); + Assert.Equal("Test", retrieved!.Name); + } + + [Fact] + public void AddAuction_DuplicateId_DoesNotAdd() + { + var service = new AuctionStateService(); + var auction1 = new AuctionInfo { AuctionId = "123", Name = "First" }; + var auction2 = new AuctionInfo { AuctionId = "123", Name = "Second" }; + + service.AddAuction(auction1); + service.AddAuction(auction2); + + Assert.Equal("First", service.GetAuction("123")!.Name); + } + + [Fact] + public void AddAuction_FiresOnAuctionAddedEvent() + { + var service = new AuctionStateService(); + string? addedId = null; + service.OnAuctionAdded += id => addedId = id; + + service.AddAuction(new AuctionInfo { AuctionId = "123" }); + + Assert.Equal("123", addedId); + } + + [Fact] + public void AddAuction_FiresOnStateChangedEvent() + { + var service = new AuctionStateService(); + var fired = false; + service.OnStateChanged += () => fired = true; + + service.AddAuction(new AuctionInfo { AuctionId = "123" }); + + Assert.True(fired); + } + + // === RemoveAuction === + + [Fact] + public void RemoveAuction_ExistingAuction_Removes() + { + var service = new AuctionStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "123" }); + + service.RemoveAuction("123"); + + Assert.Null(service.GetAuction("123")); + } + + [Fact] + public void RemoveAuction_NonExistent_DoesNotThrow() + { + var service = new AuctionStateService(); + service.RemoveAuction("non-existent"); // Should not throw + } + + [Fact] + public void RemoveAuction_FiresOnAuctionRemovedEvent() + { + var service = new AuctionStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "123" }); + string? removedId = null; + service.OnAuctionRemoved += id => removedId = id; + + service.RemoveAuction("123"); + + Assert.Equal("123", removedId); + } + + // === UpdateAuction === + + [Fact] + public void UpdateAuction_ExistingAuction_AppliesUpdateAction() + { + var service = new AuctionStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "123", Name = "Before" }); + + service.UpdateAuction("123", a => a.Name = "After"); + + Assert.Equal("After", service.GetAuction("123")!.Name); + } + + [Fact] + public void UpdateAuction_NonExistent_DoesNotThrow() + { + var service = new AuctionStateService(); + service.UpdateAuction("non-existent", a => a.Name = "test"); // Should not throw + } + + [Fact] + public void UpdateAuction_FiresOnAuctionUpdatedEvent() + { + var service = new AuctionStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "123" }); + string? updatedId = null; + service.OnAuctionUpdated += id => updatedId = id; + + service.UpdateAuction("123", a => a.Name = "Updated"); + + Assert.Equal("123", updatedId); + } + + // === GetAllAuctions === + + [Fact] + public void GetAllAuctions_Empty_ReturnsEmpty() + { + var service = new AuctionStateService(); + Assert.Empty(service.GetAllAuctions()); + } + + [Fact] + public void GetAllAuctions_MultipleAuctions_ReturnsAll() + { + var service = new AuctionStateService(); + service.AddAuction(new AuctionInfo { AuctionId = "1" }); + service.AddAuction(new AuctionInfo { AuctionId = "2" }); + service.AddAuction(new AuctionInfo { AuctionId = "3" }); + + Assert.Equal(3, service.GetAllAuctions().Count()); + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/AuctionStatisticsTests.cs b/Mimante/Tests/AutoBidder.Tests/AuctionStatisticsTests.cs new file mode 100644 index 0000000..9c99346 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/AuctionStatisticsTests.cs @@ -0,0 +1,192 @@ +using AutoBidder.Models; +using Xunit; + +namespace AutoBidder.Tests; + +public class AuctionStatisticsTests +{ + // === Calculate === + + [Fact] + public void Calculate_EmptyBidHistory_ReturnsZeroStats() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + AddedAt = DateTime.UtcNow.AddMinutes(-10) + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.Equal("1", stats.AuctionId); + Assert.Equal("Test", stats.Name); + Assert.Equal(0, stats.TotalBids); + Assert.Equal(0, stats.MyBids); + } + + [Fact] + public void Calculate_WithBidHistory_CountsMyBids() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + AddedAt = DateTime.UtcNow.AddMinutes(-10), + BidHistory = new List + { + new() { EventType = BidEventType.MyBid, Price = 1.0, Timer = 5 }, + new() { EventType = BidEventType.MyBid, Price = 2.0, Timer = 4 }, + new() { EventType = BidEventType.OpponentBid, Price = 3.0, Timer = 3 }, + } + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.Equal(2, stats.MyBids); + Assert.Equal(3, stats.TotalBids); + Assert.Equal(1, stats.OpponentBids); + } + + [Fact] + public void Calculate_WithPrices_ComputesPriceStats() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + AddedAt = DateTime.UtcNow.AddMinutes(-10), + BidHistory = new List + { + new() { EventType = BidEventType.MyBid, Price = 1.0 }, + new() { EventType = BidEventType.OpponentBid, Price = 3.0 }, + new() { EventType = BidEventType.MyBid, Price = 5.0 }, + } + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.Equal(1.0, stats.StartPrice); + Assert.Equal(5.0, stats.CurrentPrice); + Assert.Equal(1.0, stats.MinPrice); + Assert.Equal(5.0, stats.MaxPrice); + Assert.Equal(3.0, stats.AvgPrice, 2); + } + + [Fact] + public void Calculate_WithLatency_ComputesLatencyStats() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + AddedAt = DateTime.UtcNow.AddMinutes(-10), + BidHistory = new List + { + new() { EventType = BidEventType.MyBid, Price = 1.0, LatencyMs = 50 }, + new() { EventType = BidEventType.MyBid, Price = 2.0, LatencyMs = 100 }, + } + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.Equal(75, stats.AvgClickLatencyMs); + Assert.Equal(50, stats.MinClickLatencyMs); + Assert.Equal(100, stats.MaxClickLatencyMs); + } + + [Fact] + public void Calculate_WithBidderStats_FindsMostActive() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + AddedAt = DateTime.UtcNow.AddMinutes(-10), + BidderStats = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["alice"] = new BidderInfo { Username = "alice", BidCount = 10 }, + ["bob"] = new BidderInfo { Username = "bob", BidCount = 25 }, + } + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.Equal("bob", stats.MostActiveBidder); + Assert.Equal(25, stats.MostActiveBidderCount); + Assert.Equal(2, stats.UniqueBidders); + } + + [Fact] + public void Calculate_SetsResets() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + ResetCount = 42, + AddedAt = DateTime.UtcNow.AddMinutes(-60) + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.Equal(42, stats.Resets); + } + + [Fact] + public void Calculate_MonitoringDuration_IsPositive() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + AddedAt = DateTime.UtcNow.AddHours(-2) + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.True(stats.MonitoringDuration.TotalHours >= 1.9); + } + + [Fact] + public void Calculate_WithBidsAndDuration_ComputesBidsPerMinute() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + AddedAt = DateTime.UtcNow.AddMinutes(-10), + BidHistory = new List + { + new() { EventType = BidEventType.MyBid, Price = 1.0 }, + new() { EventType = BidEventType.OpponentBid, Price = 2.0 }, + } + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.True(stats.BidsPerMinute > 0); + } + + [Fact] + public void Calculate_MyBidSuccessRate_CorrectPercentage() + { + var auction = new AuctionInfo + { + AuctionId = "1", + Name = "Test", + AddedAt = DateTime.UtcNow.AddMinutes(-10), + BidHistory = new List + { + new() { EventType = BidEventType.MyBid, Price = 1.0 }, + new() { EventType = BidEventType.OpponentBid, Price = 2.0 }, + new() { EventType = BidEventType.OpponentBid, Price = 3.0 }, + new() { EventType = BidEventType.OpponentBid, Price = 4.0 }, + } + }; + + var stats = AuctionStatistics.Calculate(auction); + + Assert.Equal(25.0, stats.MyBidSuccessRate, 1); // 1 out of 4 + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/AutoBidder.Tests.csproj b/Mimante/Tests/AutoBidder.Tests/AutoBidder.Tests.csproj new file mode 100644 index 0000000..f712579 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/AutoBidder.Tests.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + diff --git a/Mimante/Tests/AutoBidder.Tests/BidStrategyServiceTests.cs b/Mimante/Tests/AutoBidder.Tests/BidStrategyServiceTests.cs new file mode 100644 index 0000000..00a55e2 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/BidStrategyServiceTests.cs @@ -0,0 +1,303 @@ +using AutoBidder.Models; +using AutoBidder.Services; +using AutoBidder.Utilities; +using Xunit; + +namespace AutoBidder.Tests; + +public class BidStrategyServiceTests +{ + private readonly BidStrategyService _service = new(); + + // === UpdateHeatMetric === + + [Fact] + public void UpdateHeatMetric_CompetitionDisabled_DoesNothing() + { + var auction = CreateAuction(); + var settings = new AppSettings { CompetitionDetectionEnabled = false }; + + _service.UpdateHeatMetric(auction, settings, "me"); + + Assert.Equal(0, auction.HeatMetric); + } + + [Fact] + public void UpdateHeatMetric_WithRecentBids_CalculatesHeat() + { + var auction = CreateAuction(); + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + auction.RecentBids.Add(new BidHistoryEntry { Username = "alice", Timestamp = now - 5, Price = 1 }); + auction.RecentBids.Add(new BidHistoryEntry { Username = "bob", Timestamp = now - 5, Price = 2 }); + auction.RecentBids.Add(new BidHistoryEntry { Username = "charlie", Timestamp = now - 10, Price = 3 }); + + var settings = new AppSettings + { + CompetitionDetectionEnabled = true, + CompetitionWindowSeconds = 30, + OpponentProfilingEnabled = false + }; + + _service.UpdateHeatMetric(auction, settings, "me"); + + Assert.True(auction.ActiveBiddersCount >= 3); + Assert.True(auction.HeatMetric > 0); + } + + [Fact] + public void UpdateHeatMetric_ExcludesCurrentUser() + { + var auction = CreateAuction(); + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + auction.RecentBids.Add(new BidHistoryEntry { Username = "me", Timestamp = now - 5, Price = 1 }); + auction.RecentBids.Add(new BidHistoryEntry { Username = "me", Timestamp = now - 10, Price = 2 }); + + var settings = new AppSettings + { + CompetitionDetectionEnabled = true, + CompetitionWindowSeconds = 30, + OpponentProfilingEnabled = false + }; + + _service.UpdateHeatMetric(auction, settings, "me"); + + Assert.Equal(0, auction.ActiveBiddersCount); + } + + [Fact] + public void UpdateHeatMetric_DetectsCollisions() + { + var auction = CreateAuction(); + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + // Two bids at the same second = collision + auction.RecentBids.Add(new BidHistoryEntry { Username = "alice", Timestamp = now - 5, Price = 1 }); + auction.RecentBids.Add(new BidHistoryEntry { Username = "bob", Timestamp = now - 5, Price = 2 }); + + var settings = new AppSettings + { + CompetitionDetectionEnabled = true, + CompetitionWindowSeconds = 30, + OpponentProfilingEnabled = false + }; + + _service.UpdateHeatMetric(auction, settings, "me"); + + Assert.True(auction.CollisionCount >= 1); + } + + // === ShouldPlaceBid === + + [Fact] + public void ShouldPlaceBid_AdvancedStrategiesDisabled_ReturnsTrue() + { + var auction = CreateAuction(); + auction.AdvancedStrategiesEnabled = false; + var state = new AuctionState { Price = 1.0, LastBidder = "other" }; + var settings = new AppSettings(); + + var decision = _service.ShouldPlaceBid(auction, state, settings, "me"); + + Assert.True(decision.ShouldBid); + } + + [Fact] + public void ShouldPlaceBid_InSoftRetreat_ReturnsFalse() + { + var auction = CreateAuction(); + auction.IsInSoftRetreat = true; + auction.LastSoftRetreatAt = DateTime.UtcNow; + var state = new AuctionState { Price = 1.0 }; + var settings = new AppSettings + { + SoftRetreatEnabled = true, + SoftRetreatDurationSeconds = 300 // Long retreat + }; + + var decision = _service.ShouldPlaceBid(auction, state, settings, "me"); + + Assert.False(decision.ShouldBid); + Assert.Contains("Soft retreat", decision.Reason); + } + + [Fact] + public void ShouldPlaceBid_SoftRetreatExpired_ReturnsTrue() + { + var auction = CreateAuction(); + auction.IsInSoftRetreat = true; + auction.LastSoftRetreatAt = DateTime.UtcNow.AddSeconds(-100); + var state = new AuctionState { Price = 1.0 }; + var settings = new AppSettings + { + SoftRetreatEnabled = true, + SoftRetreatDurationSeconds = 10, + CompetitionDetectionEnabled = false, + OpponentProfilingEnabled = false, + ProbabilisticBiddingEnabled = false, + BankrollManagerEnabled = false, + AntiBotDetectionEnabled = false, + UserExhaustionEnabled = false + }; + + var decision = _service.ShouldPlaceBid(auction, state, settings, "me"); + + Assert.True(decision.ShouldBid); + Assert.False(auction.IsInSoftRetreat); + } + + [Fact] + public void ShouldPlaceBid_TooManyCollisions_ActivatesSoftRetreat() + { + var auction = CreateAuction(); + auction.ConsecutiveCollisions = 5; + var state = new AuctionState { Price = 1.0 }; + var settings = new AppSettings + { + SoftRetreatEnabled = true, + SoftRetreatAfterCollisions = 3, + SoftRetreatDurationSeconds = 30, + CompetitionDetectionEnabled = false, + OpponentProfilingEnabled = false, + ProbabilisticBiddingEnabled = false, + BankrollManagerEnabled = false, + AntiBotDetectionEnabled = false, + UserExhaustionEnabled = false + }; + + var decision = _service.ShouldPlaceBid(auction, state, settings, "me"); + + Assert.False(decision.ShouldBid); + Assert.True(auction.IsInSoftRetreat); + } + + [Fact] + public void ShouldPlaceBid_AggressiveBiddersAvoid_ReturnsFalse() + { + var auction = CreateAuction(); + auction.AggressiveBidders.Add("aggro_user"); + var state = new AuctionState { Price = 1.0 }; + var settings = new AppSettings + { + SoftRetreatEnabled = false, + CompetitionDetectionEnabled = false, + OpponentProfilingEnabled = true, + AggressiveBidderAction = "Avoid", + ProbabilisticBiddingEnabled = false, + BankrollManagerEnabled = false, + AntiBotDetectionEnabled = false, + UserExhaustionEnabled = false + }; + + var decision = _service.ShouldPlaceBid(auction, state, settings, "me"); + + Assert.False(decision.ShouldBid); + Assert.Contains("aggressivi", decision.Reason); + } + + [Fact] + public void ShouldPlaceBid_BankrollLimitReached_ReturnsFalse() + { + var auction = CreateAuction(); + auction.SessionBidCount = 100; + auction.MaxBidsOverride = 50; + var state = new AuctionState { Price = 1.0 }; + var settings = new AppSettings + { + SoftRetreatEnabled = false, + CompetitionDetectionEnabled = false, + OpponentProfilingEnabled = false, + ProbabilisticBiddingEnabled = false, + BankrollManagerEnabled = true, + MaxBidsPerAuction = 50, + AntiBotDetectionEnabled = false, + UserExhaustionEnabled = false + }; + + var decision = _service.ShouldPlaceBid(auction, state, settings, "me"); + + Assert.False(decision.ShouldBid); + } + + // === RecordBidAttempt === + + [Fact] + public void RecordBidAttempt_Success_IncrementsCountsAndResetsCollisions() + { + var auction = CreateAuction(); + auction.ConsecutiveCollisions = 3; + + _service.RecordBidAttempt(auction, success: true, collision: false); + + Assert.Equal(1, auction.SessionBidCount); + Assert.Equal(1, auction.SuccessfulBidCount); + Assert.Equal(0, auction.ConsecutiveCollisions); + } + + [Fact] + public void RecordBidAttempt_FailureWithCollision_IncrementsCollisionCounters() + { + var auction = CreateAuction(); + + _service.RecordBidAttempt(auction, success: false, collision: true); + + Assert.Equal(1, auction.SessionBidCount); + Assert.Equal(1, auction.FailedBidCount); + Assert.Equal(1, auction.CollisionCount); + Assert.Equal(1, auction.ConsecutiveCollisions); + } + + // === RecordTimerExpired === + + [Fact] + public void RecordTimerExpired_IncrementsCounters() + { + var auction = CreateAuction(); + + _service.RecordTimerExpired(auction); + + Assert.Equal(1, auction.TimerExpiredCount); + Assert.Equal(1, auction.ConsecutiveCollisions); + } + + // === ResetSession === + + [Fact] + public void ResetSession_ClearsSessionTotals() + { + var auction = CreateAuction(); + _service.RecordBidAttempt(auction, true); + _service.RecordBidAttempt(auction, true); + + _service.ResetSession(); + + var stats = _service.GetSessionStats(); + Assert.Equal(0, stats.TotalBids); + } + + // === GetSessionStats === + + [Fact] + public void GetSessionStats_ReturnsCorrectTotals() + { + var auction = CreateAuction(); + _service.RecordBidAttempt(auction, true); + _service.RecordBidAttempt(auction, false); + + var stats = _service.GetSessionStats(); + + Assert.Equal(2, stats.TotalBids); + Assert.True(stats.SessionDuration >= TimeSpan.Zero); + } + + // === Helpers === + + private static AuctionInfo CreateAuction() + { + return new AuctionInfo + { + AuctionId = "test", + Name = "Test Auction", + OriginalUrl = "https://test.com", + AdvancedStrategiesEnabled = true + }; + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/ClosedAuctionsScraperTests.cs b/Mimante/Tests/AutoBidder.Tests/ClosedAuctionsScraperTests.cs new file mode 100644 index 0000000..5176183 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/ClosedAuctionsScraperTests.cs @@ -0,0 +1,187 @@ +using AutoBidder.Services; +using Xunit; + +namespace AutoBidder.Tests; + +public class ClosedAuctionsScraperTests +{ + // The scraper's parsing methods are private, but we can test them + // indirectly through the public ScrapeYieldAsync and ScrapeAsync methods + // using a mock HTTP handler. + + // === Constructor === + + [Fact] + public void Constructor_DefaultHandler_DoesNotThrow() + { + var scraper = new ClosedAuctionsScraper(); + Assert.NotNull(scraper); + } + + [Fact] + public void Constructor_WithLogCallback_DoesNotThrow() + { + var messages = new List(); + var scraper = new ClosedAuctionsScraper(log: msg => messages.Add(msg)); + Assert.NotNull(scraper); + } + + // === ScrapeAsync with mock handler === + + [Fact] + public async Task ScrapeAsync_EmptyUrl_ThrowsArgumentNullException() + { + var scraper = new ClosedAuctionsScraper(); + await Assert.ThrowsAsync(() => scraper.ScrapeAsync("")); + } + + [Fact] + public async Task ScrapeAsync_WhitespaceUrl_ThrowsArgumentNullException() + { + var scraper = new ClosedAuctionsScraper(); + await Assert.ThrowsAsync(() => scraper.ScrapeAsync(" ")); + } + + [Fact] + public async Task ScrapeAsync_WithMockHandler_ParsesAuctionLinks() + { + var closedPageHtml = @" + + +
+ iPhone 15 Pro + 5,50 € + winner123 +
+ + "; + + var auctionPageHtml = @" + + iPhone 15 Pro - Asta Bidoo + +

iPhone 15 Pro

+ 5,50 € + Vincitore: winner123 +

128 Puntate utilizzate

+ + "; + + var handler = new MockHttpHandler(new Dictionary + { + ["https://it.bidoo.com/closed-auctions"] = closedPageHtml, + ["https://it.bidoo.com/asta/iphone-15-12345"] = auctionPageHtml + }); + + var scraper = new ClosedAuctionsScraper(handler); + var results = await scraper.ScrapeAsync("https://it.bidoo.com/closed-auctions"); + + Assert.NotEmpty(results); + var first = results[0]; + Assert.NotNull(first.ProductName); + Assert.NotNull(first.AuctionUrl); + } + + [Fact] + public async Task ScrapeYieldAsync_ProducesRecordsIncrementally() + { + var closedPageHtml = @" + + +
+
+ + "; + + var auctionHtml = @"Test

Test Product

1,00 €"; + + var handler = new MockHttpHandler(new Dictionary + { + ["https://it.bidoo.com/closed"] = closedPageHtml, + ["https://it.bidoo.com/asta/test-1"] = auctionHtml, + ["https://it.bidoo.com/asta/test-2"] = auctionHtml + }); + + var scraper = new ClosedAuctionsScraper(handler); + var count = 0; + await foreach (var record in scraper.ScrapeYieldAsync("https://it.bidoo.com/closed")) + { + count++; + Assert.NotNull(record); + } + + Assert.Equal(2, count); + } + + [Fact] + public async Task ScrapeAsync_FailedAuctionPage_ReturnsParseErrorRecord() + { + var closedPageHtml = @"
"; + + var handler = new MockHttpHandler(new Dictionary + { + ["https://it.bidoo.com/closed"] = closedPageHtml + // Intentionally no mapping for /asta/fail-1 -> will fail + }); + + var scraper = new ClosedAuctionsScraper(handler); + var results = await scraper.ScrapeAsync("https://it.bidoo.com/closed"); + + Assert.Single(results); + Assert.Equal("(parse error)", results[0].ProductName); + } + + [Fact] + public async Task ScrapeAsync_ExtractsBidsUsed() + { + var closedPageHtml = @"
"; + var auctionHtml = @" + +

Test Product

+

puntate456 Puntate utilizzate

+ "; + + var handler = new MockHttpHandler(new Dictionary + { + ["https://it.bidoo.com/closed"] = closedPageHtml, + ["https://it.bidoo.com/asta/bid-test"] = auctionHtml + }); + + var scraper = new ClosedAuctionsScraper(handler); + var results = await scraper.ScrapeAsync("https://it.bidoo.com/closed"); + + Assert.Single(results); + Assert.Equal(456, results[0].BidsUsed); + } + + /// + /// Simple mock HTTP handler for testing + /// + private class MockHttpHandler : HttpMessageHandler + { + private readonly Dictionary _responses; + + public MockHttpHandler(Dictionary responses) + { + _responses = responses; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var url = request.RequestUri!.ToString(); + + if (_responses.TryGetValue(url, out var content)) + { + return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(content, System.Text.Encoding.UTF8, "text/html") + }); + } + + return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.NotFound) + { + Content = new StringContent("Not Found") + }); + } + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/HtmlCacheServiceTests.cs b/Mimante/Tests/AutoBidder.Tests/HtmlCacheServiceTests.cs new file mode 100644 index 0000000..0756089 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/HtmlCacheServiceTests.cs @@ -0,0 +1,77 @@ +using AutoBidder.Services; +using Xunit; + +namespace AutoBidder.Tests; + +public class HtmlCacheServiceTests +{ + // === Constructor / GetStats === + + [Fact] + public void Constructor_InitializesWithDefaults() + { + var service = new HtmlCacheService(); + var stats = service.GetStats(); + + Assert.Equal(0, stats.TotalEntries); + Assert.True(stats.AvailableSlots > 0); + Assert.True(stats.MaxConcurrent > 0); + } + + [Fact] + public void Constructor_CustomParameters_ReflectsInStats() + { + var service = new HtmlCacheService(maxConcurrentRequests: 5); + var stats = service.GetStats(); + + Assert.Equal(5, stats.MaxConcurrent); + Assert.Equal(5, stats.AvailableSlots); + } + + // === ClearCache === + + [Fact] + public void ClearCache_EmptyCache_DoesNotThrow() + { + var service = new HtmlCacheService(); + service.ClearCache(); + + Assert.Equal(0, service.GetStats().TotalEntries); + } + + // === CleanExpiredCache === + + [Fact] + public void CleanExpiredCache_EmptyCache_DoesNotThrow() + { + var service = new HtmlCacheService(); + service.CleanExpiredCache(); + + Assert.Equal(0, service.GetStats().TotalEntries); + } + + // === OnLog callback === + + [Fact] + public void ClearCache_InvokesOnLog() + { + var service = new HtmlCacheService(); + var logged = false; + service.OnLog = _ => logged = true; + + service.ClearCache(); + + Assert.True(logged); + } + + // === GetStats === + + [Fact] + public void GetStats_ReturnsNonNullResult() + { + var service = new HtmlCacheService(); + var stats = service.GetStats(); + + Assert.NotNull(stats); + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/ModelTests.cs b/Mimante/Tests/AutoBidder.Tests/ModelTests.cs new file mode 100644 index 0000000..e57aff2 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/ModelTests.cs @@ -0,0 +1,314 @@ +using AutoBidder.Models; +using Xunit; + +namespace AutoBidder.Tests; + +public class ModelTests +{ + // === BidooSession.IsValid === + + [Fact] + public void BidooSession_IsValid_FalseWhenEmpty() + { + var session = new BidooSession(); + Assert.False(session.IsValid); + } + + [Fact] + public void BidooSession_IsValid_TrueWithAuthToken() + { + var session = new BidooSession { AuthToken = "abc123" }; + Assert.True(session.IsValid); + } + + [Fact] + public void BidooSession_IsValid_TrueWithCookieString() + { + var session = new BidooSession { CookieString = "session=abc" }; + Assert.True(session.IsValid); + } + + [Fact] + public void BidooSession_IsValid_FalseWithWhitespaceOnly() + { + var session = new BidooSession { AuthToken = " ", CookieString = " " }; + Assert.False(session.IsValid); + } + + // === ProductStatisticsRecord.WinRate === + + [Fact] + public void ProductStatisticsRecord_WinRate_ZeroWhenNoAuctions() + { + var record = new ProductStatisticsRecord { TotalAuctions = 0, WonAuctions = 0 }; + Assert.Equal(0, record.WinRate); + } + + [Fact] + public void ProductStatisticsRecord_WinRate_CorrectPercentage() + { + var record = new ProductStatisticsRecord { TotalAuctions = 10, WonAuctions = 3 }; + Assert.Equal(30.0, record.WinRate, 1); + } + + [Fact] + public void ProductStatisticsRecord_WinRate_100Percent() + { + var record = new ProductStatisticsRecord { TotalAuctions = 5, WonAuctions = 5 }; + Assert.Equal(100.0, record.WinRate, 1); + } + + // === HourlyStats.WinRate === + + [Fact] + public void HourlyStats_WinRate_ZeroWhenNoAuctions() + { + var stats = new HourlyStats { TotalAuctions = 0, WonAuctions = 0 }; + Assert.Equal(0, stats.WinRate); + } + + [Fact] + public void HourlyStats_WinRate_CorrectPercentage() + { + var stats = new HourlyStats { TotalAuctions = 20, WonAuctions = 5 }; + Assert.Equal(25.0, stats.WinRate, 1); + } + + // === CompleteAuctionHistoryRecord === + + [Fact] + public void CompleteAuctionHistoryRecord_DurationFormatted_WithDuration() + { + var record = new CompleteAuctionHistoryRecord { DurationSeconds = 3661 }; // 1h 1m 1s + Assert.Equal("01:01:01", record.DurationFormatted); + } + + [Fact] + public void CompleteAuctionHistoryRecord_DurationFormatted_NoDuration() + { + var record = new CompleteAuctionHistoryRecord { DurationSeconds = null }; + Assert.Equal("-", record.DurationFormatted); + } + + [Fact] + public void CompleteAuctionHistoryRecord_SuccessRate_ZeroWhenNoBids() + { + var record = new CompleteAuctionHistoryRecord { MySuccessfulBids = 0, MyFailedBids = 0 }; + Assert.Equal(0, record.SuccessRate); + } + + [Fact] + public void CompleteAuctionHistoryRecord_SuccessRate_CorrectPercentage() + { + var record = new CompleteAuctionHistoryRecord { MySuccessfulBids = 7, MyFailedBids = 3 }; + Assert.Equal(70.0, record.SuccessRate, 1); + } + + // === ProductStat === + + [Fact] + public void ProductStat_AverageBidsUsed_ZeroWhenNoAuctions() + { + var stat = new ProductStat { TotalAuctions = 0, TotalBidsUsed = 0 }; + Assert.Equal(0, stat.AverageBidsUsed); + } + + [Fact] + public void ProductStat_AverageBidsUsed_CorrectAverage() + { + var stat = new ProductStat { TotalAuctions = 5, TotalBidsUsed = 100 }; + Assert.Equal(20.0, stat.AverageBidsUsed, 1); + } + + [Fact] + public void ProductStat_AverageFinalPrice_ZeroWhenNoAuctions() + { + var stat = new ProductStat { TotalAuctions = 0, TotalFinalPriceCents = 0 }; + Assert.Equal(0, stat.AverageFinalPrice); + } + + [Fact] + public void ProductStat_AverageFinalPrice_CorrectAverage() + { + var stat = new ProductStat { TotalAuctions = 2, TotalFinalPriceCents = 1000 }; + Assert.Equal(5.0, stat.AverageFinalPrice, 2); // 1000 cents / 100 / 2 = 5.0 + } + + // === BidHistoryEntry === + + [Fact] + public void BidHistoryEntry_PriceFormatted_TwoDecimals() + { + var entry = new BidHistoryEntry { Price = 5.5m }; + // PriceFormatted uses default culture formatting + Assert.Equal(5.5m.ToString("0.00"), entry.PriceFormatted); + } + + [Fact] + public void BidHistoryEntry_TimeFormatted_ValidFormat() + { + var entry = new BidHistoryEntry { Timestamp = 1700000000 }; + // Should produce a valid HH:mm:ss string + Assert.Matches(@"^\d{2}:\d{2}:\d{2}$", entry.TimeFormatted); + } + + // === BidderInfo === + + [Fact] + public void BidderInfo_LastBidTimeDisplay_DashWhenDefault() + { + var info = new BidderInfo(); + Assert.Equal("-", info.LastBidTimeDisplay); + } + + [Fact] + public void BidderInfo_LastBidTimeDisplay_FormattedWhenSet() + { + var info = new BidderInfo { LastBidTime = new DateTime(2024, 1, 1, 14, 30, 45) }; + Assert.Equal("14:30:45", info.LastBidTimeDisplay); + } + + // === AuctionLogEntry === + + [Fact] + public void AuctionLogEntry_LevelIcon_ReturnsBootstrapClass() + { + var entry = new AuctionLogEntry { Level = AuctionLogLevel.Error }; + Assert.Contains("bi-", entry.LevelIcon); + } + + [Fact] + public void AuctionLogEntry_LevelClass_ReturnsCorrectCssClass() + { + var entry = new AuctionLogEntry { Level = AuctionLogLevel.Error }; + Assert.Equal("alog-error", entry.LevelClass); + } + + [Fact] + public void AuctionLogEntry_LevelLabel_ReturnsShortLabel() + { + Assert.Equal("ERR", new AuctionLogEntry { Level = AuctionLogLevel.Error }.LevelLabel); + Assert.Equal("WARN", new AuctionLogEntry { Level = AuctionLogLevel.Warning }.LevelLabel); + Assert.Equal("OK", new AuctionLogEntry { Level = AuctionLogLevel.Success }.LevelLabel); + Assert.Equal("BID", new AuctionLogEntry { Level = AuctionLogLevel.Bid }.LevelLabel); + } + + [Fact] + public void AuctionLogEntry_CategoryLabel_ReturnsLabel() + { + var entry = new AuctionLogEntry { Category = AuctionLogCategory.BidAttempt }; + Assert.Equal("Puntata", entry.CategoryLabel); + } + + [Fact] + public void AuctionLogEntry_TimeDisplay_FormattedWithMs() + { + var entry = new AuctionLogEntry { Timestamp = new DateTime(2024, 1, 1, 14, 30, 45, 123) }; + Assert.Equal("14:30:45.123", entry.TimeDisplay); + } + + // === AuctionInfo.AddLog === + + [Fact] + public void AuctionInfo_AddLog_AddsEntry() + { + var auction = new AuctionInfo { AuctionId = "1" }; + auction.AddLog("[BID] Test bid placed"); + + Assert.Single(auction.AuctionLog); + } + + [Fact] + public void AuctionInfo_AddLog_ParsesTag() + { + var auction = new AuctionInfo { AuctionId = "1" }; + auction.AddLog("[ERROR] Something went wrong"); + + Assert.Equal(AuctionLogLevel.Error, auction.AuctionLog[0].Level); + } + + [Fact] + public void AuctionInfo_AddLog_Deduplicates() + { + var auction = new AuctionInfo { AuctionId = "1" }; + auction.AddLog("[BID] Same message"); + auction.AddLog("[BID] Same message"); + auction.AddLog("[BID] Same message"); + + Assert.Single(auction.AuctionLog); + Assert.Equal(3, auction.AuctionLog[0].RepeatCount); + } + + [Fact] + public void AuctionInfo_AddLog_TruncatesAtMaxLines() + { + var auction = new AuctionInfo { AuctionId = "1" }; + for (int i = 0; i < 300; i++) + { + auction.AddLog($"Message {i}"); + } + + Assert.True(auction.AuctionLog.Count <= 200); + } + + [Fact] + public void AuctionInfo_AddLogTyped_AddsWithLevelAndCategory() + { + var auction = new AuctionInfo { AuctionId = "1" }; + auction.AddLog("Test", AuctionLogLevel.Warning, AuctionLogCategory.Price); + + Assert.Single(auction.AuctionLog); + Assert.Equal(AuctionLogLevel.Warning, auction.AuctionLog[0].Level); + Assert.Equal(AuctionLogCategory.Price, auction.AuctionLog[0].Category); + } + + // === AuctionInfo.AddLatencyMeasurement === + + [Fact] + public void AuctionInfo_AddLatencyMeasurement_AddsToHistory() + { + var auction = new AuctionInfo { AuctionId = "1" }; + auction.AddLatencyMeasurement(50); + auction.AddLatencyMeasurement(100); + + Assert.Equal(2, auction.LatencyHistory.Count); + Assert.Equal(100, auction.PollingLatencyMs); + } + + [Fact] + public void AuctionInfo_AddLatencyMeasurement_TruncatesHistory() + { + var auction = new AuctionInfo { AuctionId = "1" }; + for (int i = 0; i < 20; i++) + { + auction.AddLatencyMeasurement(i * 10); + } + + Assert.True(auction.LatencyHistory.Count <= 10); + } + + [Fact] + public void AuctionInfo_AverageLatencyMs_ComputesAverage() + { + var auction = new AuctionInfo { AuctionId = "1" }; + auction.AddLatencyMeasurement(50); + auction.AddLatencyMeasurement(100); + + Assert.Equal(75.0, auction.AverageLatencyMs, 1); + } + + [Fact] + public void AuctionInfo_AverageLatencyMs_FallsBackToPollingLatency() + { + var auction = new AuctionInfo { AuctionId = "1", PollingLatencyMs = 80 }; + + Assert.Equal(80.0, auction.AverageLatencyMs, 1); + } + + [Fact] + public void AuctionInfo_AverageLatencyMs_Default60WhenNoData() + { + var auction = new AuctionInfo { AuctionId = "1" }; + Assert.Equal(60.0, auction.AverageLatencyMs, 1); + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/ProductInsightsTests.cs b/Mimante/Tests/AutoBidder.Tests/ProductInsightsTests.cs new file mode 100644 index 0000000..8012e49 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/ProductInsightsTests.cs @@ -0,0 +1,161 @@ +using AutoBidder.Models; +using Xunit; + +namespace AutoBidder.Tests; + +public class ProductInsightsTests +{ + // === Calculate === + + [Fact] + public void Calculate_NullAuctions_ReturnsLowConfidence() + { + var insights = ProductInsights.Calculate(null!, "key", "name"); + + Assert.Equal(0, insights.ConfidenceScore); + Assert.Contains("insufficienti", insights.RecommendedStrategy); + } + + [Fact] + public void Calculate_EmptyAuctions_ReturnsLowConfidence() + { + var insights = ProductInsights.Calculate(new List(), "key", "name"); + + Assert.Equal(0, insights.ConfidenceScore); + } + + [Fact] + public void Calculate_WithValidData_ComputesPriceStats() + { + var auctions = new List + { + new() { FinalPrice = 5.0, BidsUsed = 10, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 10.0, BidsUsed = 20, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 15.0, BidsUsed = 30, ScrapedAt = DateTime.UtcNow }, + }; + + var insights = ProductInsights.Calculate(auctions, "key", "Test Product"); + + Assert.Equal(3, insights.TotalAuctions); + Assert.Equal(10.0, insights.AverageFinalPrice, 1); + Assert.Equal(5.0, insights.MinFinalPrice, 1); + Assert.Equal(15.0, insights.MaxFinalPrice, 1); + Assert.Equal(10.0, insights.MedianFinalPrice, 1); + } + + [Fact] + public void Calculate_WithBidsData_ComputesBidsStats() + { + var auctions = new List + { + new() { FinalPrice = 5.0, BidsUsed = 10, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 10.0, BidsUsed = 20, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 15.0, BidsUsed = 30, ScrapedAt = DateTime.UtcNow }, + }; + + var insights = ProductInsights.Calculate(auctions, "key", "name"); + + Assert.Equal(20.0, insights.AverageBidsUsed, 1); + Assert.Equal(10, insights.MinBidsUsed); + Assert.Equal(30, insights.MaxBidsUsed); + } + + [Fact] + public void Calculate_SetsRecommendedValues() + { + var auctions = Enumerable.Range(1, 10) + .Select(i => new ClosedAuctionRecord + { + FinalPrice = i * 2.0, + BidsUsed = i * 5, + ScrapedAt = DateTime.UtcNow + }) + .ToList(); + + var insights = ProductInsights.Calculate(auctions, "key", "name"); + + Assert.True(insights.RecommendedMaxPrice > 0); + Assert.True(insights.RecommendedMaxBids > 0); + Assert.False(string.IsNullOrEmpty(insights.RecommendedStrategy)); + } + + [Fact] + public void Calculate_HourlyDistribution_BasedOnScrapedAt() + { + var auctions = new List + { + new() { FinalPrice = 5.0, ScrapedAt = new DateTime(2024, 1, 1, 14, 0, 0) }, + new() { FinalPrice = 10.0, ScrapedAt = new DateTime(2024, 1, 1, 14, 30, 0) }, + new() { FinalPrice = 15.0, ScrapedAt = new DateTime(2024, 1, 1, 20, 0, 0) }, + }; + + var insights = ProductInsights.Calculate(auctions, "key", "name"); + + Assert.True(insights.HourlyDistribution.ContainsKey(14)); + Assert.Equal(2, insights.HourlyDistribution[14]); + } + + [Fact] + public void Calculate_OptimalStartPrice_NonNegative() + { + var auctions = new List + { + new() { FinalPrice = 1.0, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 2.0, ScrapedAt = DateTime.UtcNow }, + }; + + var insights = ProductInsights.Calculate(auctions, "key", "name"); + + Assert.True(insights.OptimalStartPrice >= 0); + } + + [Fact] + public void Calculate_HighCompetition_StrategyContainsAlta() + { + // Create data with high bid variance relative to average (high competition intensity) + var auctions = new List + { + new() { FinalPrice = 5.0, BidsUsed = 100, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 10.0, BidsUsed = 5, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 15.0, BidsUsed = 200, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 8.0, BidsUsed = 3, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = 12.0, BidsUsed = 150, ScrapedAt = DateTime.UtcNow }, + }; + + var insights = ProductInsights.Calculate(auctions, "key", "name"); + + // CompetitionIntensity is clamped to [0,1]; just verify it's calculated + Assert.True(insights.CompetitionIntensity >= 0); + Assert.True(insights.CompetitionIntensity <= 1); + } + + [Fact] + public void Calculate_LowConfidence_StrategyContainsWarning() + { + // Very few samples = low confidence + var auctions = new List + { + new() { FinalPrice = 5.0, BidsUsed = 10, ScrapedAt = DateTime.UtcNow }, + }; + + var insights = ProductInsights.Calculate(auctions, "key", "name"); + + Assert.True(insights.ConfidenceScore < 50); + } + + [Fact] + public void Calculate_NullPricesAndBids_HandlesGracefully() + { + var auctions = new List + { + new() { FinalPrice = null, BidsUsed = null, ScrapedAt = DateTime.UtcNow }, + new() { FinalPrice = null, BidsUsed = null, ScrapedAt = DateTime.UtcNow }, + }; + + var insights = ProductInsights.Calculate(auctions, "key", "name"); + + Assert.Equal(2, insights.TotalAuctions); + Assert.Equal(0, insights.AverageFinalPrice); + Assert.Equal(0, insights.AverageBidsUsed); + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/ProductStatisticsServiceTests.cs b/Mimante/Tests/AutoBidder.Tests/ProductStatisticsServiceTests.cs new file mode 100644 index 0000000..68374e2 --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/ProductStatisticsServiceTests.cs @@ -0,0 +1,188 @@ +using AutoBidder.Models; +using AutoBidder.Services; +using Xunit; + +namespace AutoBidder.Tests; + +public class ProductStatisticsServiceTests +{ + // === GenerateProductKey === + + [Fact] + public void GenerateProductKey_NullOrWhitespace_ReturnsUnknown() + { + Assert.Equal("unknown", ProductStatisticsService.GenerateProductKey(null!)); + Assert.Equal("unknown", ProductStatisticsService.GenerateProductKey("")); + Assert.Equal("unknown", ProductStatisticsService.GenerateProductKey(" ")); + } + + [Fact] + public void GenerateProductKey_NormalizesToLowerAndTrims() + { + var key = ProductStatisticsService.GenerateProductKey(" iPhone 15 Pro "); + Assert.DoesNotContain(" ", key); + Assert.Equal(key, key.ToLowerInvariant()); + } + + [Fact] + public void GenerateProductKey_RemovesParentheses() + { + var key = ProductStatisticsService.GenerateProductKey("iPhone 15 (128GB Nero)"); + Assert.DoesNotContain("128", key); + Assert.DoesNotContain("nero", key.ToLowerInvariant()); + } + + [Fact] + public void GenerateProductKey_RemovesBrackets() + { + var key = ProductStatisticsService.GenerateProductKey("Samsung Galaxy [Special Edition]"); + Assert.DoesNotContain("special", key); + } + + [Fact] + public void GenerateProductKey_RemovesColors() + { + var keyNero = ProductStatisticsService.GenerateProductKey("iPhone nero"); + var keyBianco = ProductStatisticsService.GenerateProductKey("iPhone bianco"); + // Both should produce same base key after color removal + Assert.Equal(keyNero, keyBianco); + } + + [Fact] + public void GenerateProductKey_RemovesStorageCapacity() + { + var key128 = ProductStatisticsService.GenerateProductKey("iPhone 15 128GB"); + var key256 = ProductStatisticsService.GenerateProductKey("iPhone 15 256GB"); + Assert.Equal(key128, key256); + } + + [Fact] + public void GenerateProductKey_LimitsLengthTo50() + { + var longName = new string('a', 200); + var key = ProductStatisticsService.GenerateProductKey(longName); + Assert.True(key.Length <= 50); + } + + [Fact] + public void GenerateProductKey_SameProductDifferentVariants_SameKey() + { + var key1 = ProductStatisticsService.GenerateProductKey("Apple iPhone 15 Pro"); + var key2 = ProductStatisticsService.GenerateProductKey("Apple iPhone 15 Pro (256GB)"); + Assert.Equal(key1, key2); + } + + // === CalculateRecommendedLimits === + + [Fact] + public void CalculateRecommendedLimits_LessThan3Results_ZeroConfidence() + { + var service = CreateService(); + var results = new List + { + CreateResult(won: true, price: 5.0) + }; + + var limits = service.CalculateRecommendedLimits(results); + + Assert.Equal(0, limits.ConfidenceScore); + } + + [Fact] + public void CalculateRecommendedLimits_NoWins_ReturnsConservativeLimits() + { + var service = CreateService(); + var results = Enumerable.Range(1, 5) + .Select(i => CreateResult(won: false, price: 10.0 + i)) + .ToList(); + + var limits = service.CalculateRecommendedLimits(results); + + Assert.Equal(10, limits.ConfidenceScore); + Assert.True(limits.MinPrice > 0); + Assert.True(limits.MaxPrice > limits.MinPrice); + } + + [Fact] + public void CalculateRecommendedLimits_WithWins_CalculatesPercentilePrices() + { + var service = CreateService(); + var results = Enumerable.Range(1, 20) + .Select(i => CreateResult(won: true, price: i * 1.0, resets: i, bidsUsed: i * 5)) + .ToList(); + + var limits = service.CalculateRecommendedLimits(results); + + Assert.True(limits.MinPrice > 0); + Assert.True(limits.MaxPrice > limits.MinPrice); + Assert.True(limits.MaxBids > 0); + Assert.True(limits.ConfidenceScore >= 50); + } + + [Fact] + public void CalculateRecommendedLimits_ConfidenceScalesWithSampleSize() + { + var service = CreateService(); + + var small = Enumerable.Range(1, 5) + .Select(i => CreateResult(won: true, price: i * 1.0)) + .ToList(); + var large = Enumerable.Range(1, 50) + .Select(i => CreateResult(won: true, price: i * 1.0)) + .ToList(); + + var smallLimits = service.CalculateRecommendedLimits(small); + var largeLimits = service.CalculateRecommendedLimits(large); + + Assert.True(largeLimits.ConfidenceScore > smallLimits.ConfidenceScore); + } + + [Fact] + public void CalculateRecommendedLimits_FindsBestHourToPlay() + { + var service = CreateService(); + var results = new List(); + + // 5 wins at hour 14, 2 wins at hour 20 + for (int i = 0; i < 5; i++) + results.Add(CreateResult(won: true, price: 5.0, closedAtHour: 14)); + for (int i = 0; i < 2; i++) + results.Add(CreateResult(won: true, price: 5.0, closedAtHour: 20)); + + var limits = service.CalculateRecommendedLimits(results); + + Assert.Equal(14, limits.BestHourToPlay); + } + + // === Helpers === + + private static ProductStatisticsService CreateService() + { + // ProductStatisticsService requires DatabaseService, but CalculateRecommendedLimits + // and GenerateProductKey don't use it. We pass null through a helper constructor workaround. + // Since there's no interface, we instantiate with a real (unused) DatabaseService. + // The static methods and CalculateRecommendedLimits don't access _db. + return new ProductStatisticsService(null!); + } + + private static AuctionResultExtended CreateResult( + bool won, + double price, + int? resets = null, + int bidsUsed = 0, + int? winnerBidsUsed = null, + int? closedAtHour = null) + { + return new AuctionResultExtended + { + AuctionId = Guid.NewGuid().ToString(), + AuctionName = "Test", + FinalPrice = price, + BidsUsed = bidsUsed, + Won = won, + TotalResets = resets, + WinnerBidsUsed = winnerBidsUsed ?? (won ? bidsUsed : null), + ClosedAtHour = closedAtHour + }; + } +} diff --git a/Mimante/Tests/AutoBidder.Tests/ProductValueCalculatorTests.cs b/Mimante/Tests/AutoBidder.Tests/ProductValueCalculatorTests.cs new file mode 100644 index 0000000..abbbbca --- /dev/null +++ b/Mimante/Tests/AutoBidder.Tests/ProductValueCalculatorTests.cs @@ -0,0 +1,285 @@ +using AutoBidder.Models; +using AutoBidder.Utilities; +using Xunit; + +namespace AutoBidder.Tests; + +public class ProductValueCalculatorTests +{ + // === Calculate === + + [Fact] + public void Calculate_BasicAuction_ComputesTotalCostIfWin() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = 100.0, + ShippingCost = 5.0, + BidsUsedOnThisAuction = 10 + }; + + var result = ProductValueCalculator.Calculate(auction, currentPrice: 2.0, totalBids: 200); + + Assert.Equal(2.0, result.CurrentPrice); + Assert.Equal(200, result.TotalBids); + Assert.Equal(10, result.MyBids); + Assert.Equal(2.0, result.MyBidsCost, 2); // 10 * 0.20 + Assert.Equal(9.0, result.TotalCostIfWin, 2); // 2.0 + 2.0 + 5.0 + } + + [Fact] + public void Calculate_WithBuyNowPrice_ComputesSavings() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = 100.0, + ShippingCost = 5.0, + BidsUsedOnThisAuction = 10 + }; + + var result = ProductValueCalculator.Calculate(auction, currentPrice: 2.0, totalBids: 200); + + // buyNowTotal = 100 + 5 = 105 + // totalCostIfWin = 2.0 + 2.0 + 5.0 = 9.0 + // savings = 105 - 9 = 96 + Assert.True(result.Savings.HasValue); + Assert.Equal(96.0, result.Savings!.Value, 2); + Assert.True(result.IsWorthIt); + } + + [Fact] + public void Calculate_WithoutBuyNowPrice_IsWorthItTrue() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = null, + ShippingCost = null, + BidsUsedOnThisAuction = 5 + }; + + var result = ProductValueCalculator.Calculate(auction, currentPrice: 1.0, totalBids: 100); + + Assert.True(result.IsWorthIt); + Assert.Null(result.Savings); + Assert.Null(result.SavingsPercentage); + } + + [Fact] + public void Calculate_WhenTotalCostExceedsBuyNow_NotWorthIt() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = 5.0, + ShippingCost = 1.0, + BidsUsedOnThisAuction = 50 + }; + + // totalCostIfWin = 3.0 + 10.0 + 1.0 = 14.0 > buyNowTotal (5 + 1 = 6) + var result = ProductValueCalculator.Calculate(auction, currentPrice: 3.0, totalBids: 300); + + Assert.False(result.IsWorthIt); + Assert.True(result.Savings!.Value < 0); + } + + [Fact] + public void Calculate_ZeroBuyNowPrice_IsWorthItTrue() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = 0.0, + ShippingCost = null, + BidsUsedOnThisAuction = 1 + }; + + var result = ProductValueCalculator.Calculate(auction, currentPrice: 1.0, totalBids: 100); + + // BuyNowPrice == 0 falls into the else branch (no valid buy now price) + Assert.True(result.IsWorthIt); + Assert.Null(result.Savings); + Assert.Null(result.SavingsPercentage); + } + + [Fact] + public void Calculate_NoShippingCost_ExcludesShippingFromTotal() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = 50.0, + ShippingCost = null, + BidsUsedOnThisAuction = 5 + }; + + var result = ProductValueCalculator.Calculate(auction, currentPrice: 1.0, totalBids: 100); + + // totalCostIfWin = 1.0 + 1.0 = 2.0 (no shipping) + Assert.Equal(2.0, result.TotalCostIfWin, 2); + } + + [Fact] + public void Calculate_GeneratesSummaryString() + { + var auction = new AuctionInfo + { + AuctionId = "1", + BidCost = 0.20, + BuyNowPrice = 100.0, + ShippingCost = null, + BidsUsedOnThisAuction = 0 + }; + + var result = ProductValueCalculator.Calculate(auction, currentPrice: 5.0, totalBids: 500); + + Assert.False(string.IsNullOrEmpty(result.Summary)); + } + + // === ExtractProductInfo === + + [Fact] + public void ExtractProductInfo_EmptyHtml_ReturnsFalse() + { + var auction = new AuctionInfo { AuctionId = "1" }; + Assert.False(ProductValueCalculator.ExtractProductInfo("", auction)); + Assert.False(ProductValueCalculator.ExtractProductInfo(null!, auction)); + } + + [Fact] + public void ExtractProductInfo_ProductValueSpan_ExtractsBuyNowPrice() + { + var html = @"Valore: 18,90 €"; + var auction = new AuctionInfo { AuctionId = "1" }; + + var result = ProductValueCalculator.ExtractProductInfo(html, auction); + + Assert.True(result); + Assert.Equal(18.90, auction.BuyNowPrice!.Value, 2); + } + + [Fact] + public void ExtractProductInfo_BuyItNowButton_ExtractsBuyNowPrice() + { + var html = @"Compra a 25,50 €"; + var auction = new AuctionInfo { AuctionId = "1" }; + + var result = ProductValueCalculator.ExtractProductInfo(html, auction); + + Assert.True(result); + Assert.Equal(25.50, auction.BuyNowPrice!.Value, 2); + } + + [Fact] + public void ExtractProductInfo_CompraloOra_ExtractsBuyNowPrice() + { + var html = @"COMPRALO ORA A 42,99 €"; + var auction = new AuctionInfo { AuctionId = "1" }; + + var result = ProductValueCalculator.ExtractProductInfo(html, auction); + + Assert.True(result); + Assert.Equal(42.99, auction.BuyNowPrice!.Value, 2); + } + + [Fact] + public void ExtractProductInfo_ShippingCost_ExtractsShipping() + { + var html = @"Spese di spedizione: 4,99 €"; + var auction = new AuctionInfo { AuctionId = "1" }; + + var result = ProductValueCalculator.ExtractProductInfo(html, auction); + + Assert.True(result); + Assert.Equal(4.99, auction.ShippingCost!.Value, 2); + } + + [Fact] + public void ExtractProductInfo_TransactionCost_ExtractsAsShipping() + { + var html = @"Spese di transazione: 1,00 €"; + var auction = new AuctionInfo { AuctionId = "1" }; + + var result = ProductValueCalculator.ExtractProductInfo(html, auction); + + Assert.True(result); + Assert.Equal(1.00, auction.ShippingCost!.Value, 2); + } + + [Fact] + public void ExtractProductInfo_WinLimit_ExtractsLimit() + { + var html = @"Limiti di vincita: 1 ogni 30 giorni"; + var auction = new AuctionInfo { AuctionId = "1" }; + + var result = ProductValueCalculator.ExtractProductInfo(html, auction); + + Assert.True(result); + Assert.True(auction.HasWinLimit); + Assert.Equal("1 ogni 30 giorni", auction.WinLimitDescription); + } + + // === FormatValueMessage === + + [Fact] + public void FormatValueMessage_NoBuyNowPrice_ReturnsGenericMessage() + { + var value = new ProductValue + { + TotalCostIfWin = 5.0, + CurrentPrice = 3.0, + MyBidsCost = 2.0, + BuyNowPrice = null + }; + + var message = ProductValueCalculator.FormatValueMessage(value); + + Assert.Contains("5,00", message); + } + + [Fact] + public void FormatValueMessage_IsWorthIt_ContainsConveniente() + { + var value = new ProductValue + { + TotalCostIfWin = 10.0, + CurrentPrice = 5.0, + MyBidsCost = 5.0, + BuyNowPrice = 100.0, + IsWorthIt = true, + Savings = 90.0, + SavingsPercentage = 90.0 + }; + + var message = ProductValueCalculator.FormatValueMessage(value); + + Assert.Contains("Conveniente", message); + } + + [Fact] + public void FormatValueMessage_NotWorthIt_ContainsNonConveniente() + { + var value = new ProductValue + { + TotalCostIfWin = 120.0, + CurrentPrice = 100.0, + MyBidsCost = 20.0, + BuyNowPrice = 50.0, + IsWorthIt = false, + Savings = -70.0, + SavingsPercentage = -140.0 + }; + + var message = ProductValueCalculator.FormatValueMessage(value); + + Assert.Contains("Non conveniente", message); + } +}