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
This commit is contained in:
2026-04-29 16:09:25 +02:00
parent 766d996e44
commit 711cc11805
17 changed files with 2379 additions and 15 deletions
+2 -1
View File
@@ -42,7 +42,8 @@ bld/
**/packages/*
!**/packages/build/
# Test results
# Test project and results
Tests/
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
+10 -3
View File
@@ -11,7 +11,7 @@ on:
workflow_dispatch: # Permette trigger manuale
env:
DOTNET_VERSION: '8.0.x'
DOTNET_VERSION: '10.0.x'
REGISTRY: ${{ secrets.GITEA_REGISTRY }}
jobs:
@@ -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
+10 -3
View File
@@ -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
+13 -6
View File
@@ -25,6 +25,12 @@
</PropertyGroup>
<ItemGroup>
<!-- Exclude test project from main build -->
<Compile Remove="Tests\AutoBidder.Tests\**" />
<Content Remove="Tests\AutoBidder.Tests\**" />
<EmbeddedResource Remove="Tests\AutoBidder.Tests\**" />
<None Remove="Tests\AutoBidder.Tests\**" />
<!-- Exclude WPF files from compilation -->
<Compile Remove=".github\**" />
<Compile Remove=".vscode\**" />
@@ -84,6 +90,7 @@
<None Include="Dockerfile" />
<None Include=".dockerignore" />
<None Include="Properties\PublishProfiles\GiteaRegistry-Versioned.pubxml.user" />
<None Include="Tests\AutoBidder.Tests\AutoBidder.Tests.csproj" />
</ItemGroup>
<!-- ============================================ -->
@@ -100,14 +107,14 @@
<Message Importance="high" Text="" />
<Message Importance="high" Text="+-------------------------------------------------------------------+" />
<Message Importance="high" Text="¦ POST-BUILD: Pubblicazione su Gitea Container Registry ¦" />
<Message Importance="high" Text=" POST-BUILD: Pubblicazione su Gitea Container Registry " />
<Message Importance="high" Text="+-------------------------------------------------------------------+" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="?? Solution Version: $(Version)" />
<Message Importance="high" Text="?? Local Image: $(LocalImageName):latest" />
<Message Importance="high" Text="??? Target Tags:" />
<Message Importance="high" Text=" $(GiteaImageLatest)" />
<Message Importance="high" Text=" $(GiteaImageVersion)" />
<Message Importance="high" Text=" $(GiteaImageLatest)" />
<Message Importance="high" Text=" $(GiteaImageVersion)" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="-------------------------------------------------------------------" />
<Message Importance="high" Text="??? Tagging images..." />
@@ -136,15 +143,15 @@
<Message Importance="high" Text="" />
<Message Importance="high" Text="+-------------------------------------------------------------------+" />
<Message Importance="high" Text="¦ ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ¦" />
<Message Importance="high" Text=" ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! " />
<Message Importance="high" Text="+-------------------------------------------------------------------+" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="?? Visualizza su Gitea:" />
<Message Importance="high" Text=" https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="?? Tag pubblicati:" />
<Message Importance="high" Text=" latest (sempre aggiornato all'ultima versione)" />
<Message Importance="high" Text=" $(Version) (versione solution corrente)" />
<Message Importance="high" Text=" latest (sempre aggiornato all'ultima versione)" />
<Message Importance="high" Text=" $(Version) (versione solution corrente)" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="?? Pull command:" />
<Message Importance="high" Text=" docker pull $(GiteaImageLatest)" />
+28 -1
View File
@@ -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
@@ -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<AuctionInfo>
{
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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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<BidHistory>
{
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<BidHistory>
{
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<BidHistory>
{
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<string, BidderInfo>(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<BidHistory>
{
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<BidHistory>
{
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
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\AutoBidder.csproj" />
</ItemGroup>
</Project>
@@ -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
};
}
}
@@ -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<string>();
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<ArgumentNullException>(() => scraper.ScrapeAsync(""));
}
[Fact]
public async Task ScrapeAsync_WhitespaceUrl_ThrowsArgumentNullException()
{
var scraper = new ClosedAuctionsScraper();
await Assert.ThrowsAsync<ArgumentNullException>(() => scraper.ScrapeAsync(" "));
}
[Fact]
public async Task ScrapeAsync_WithMockHandler_ParsesAuctionLinks()
{
var closedPageHtml = @"
<html>
<body>
<div data-href=""/asta/iphone-15-12345"">
<b class=""media-heading""><a href=""#"">iPhone 15 Pro</a></b>
<span class=""price"">5,50 €</span>
<span class=""mobile_offerer offer"">winner123</span>
</div>
</body>
</html>";
var auctionPageHtml = @"
<html>
<head><title>iPhone 15 Pro - Asta Bidoo</title></head>
<body>
<h1>iPhone 15 Pro</h1>
<span class=""price"">5,50 €</span>
<span>Vincitore: <a>winner123</a></span>
<p class=""bids-used""><span>128</span> Puntate utilizzate</p>
</body>
</html>";
var handler = new MockHttpHandler(new Dictionary<string, string>
{
["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 = @"
<html>
<body>
<div data-href=""/asta/test-1""></div>
<div data-href=""/asta/test-2""></div>
</body>
</html>";
var auctionHtml = @"<html><head><title>Test</title></head><body><h1>Test Product</h1><span class=""price"">1,00 €</span></body></html>";
var handler = new MockHttpHandler(new Dictionary<string, string>
{
["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 = @"<html><body><div data-href=""/asta/fail-1""></div></body></html>";
var handler = new MockHttpHandler(new Dictionary<string, string>
{
["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 = @"<html><body><div data-href=""/asta/bid-test""></div></body></html>";
var auctionHtml = @"
<html><body>
<h1>Test Product</h1>
<p class=""bids-used""> puntate<span>456</span> Puntate utilizzate</p>
</body></html>";
var handler = new MockHttpHandler(new Dictionary<string, string>
{
["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);
}
/// <summary>
/// Simple mock HTTP handler for testing
/// </summary>
private class MockHttpHandler : HttpMessageHandler
{
private readonly Dictionary<string, string> _responses;
public MockHttpHandler(Dictionary<string, string> responses)
{
_responses = responses;
}
protected override Task<HttpResponseMessage> 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")
});
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<ClosedAuctionRecord>(), "key", "name");
Assert.Equal(0, insights.ConfidenceScore);
}
[Fact]
public void Calculate_WithValidData_ComputesPriceStats()
{
var auctions = new List<ClosedAuctionRecord>
{
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<ClosedAuctionRecord>
{
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<ClosedAuctionRecord>
{
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<ClosedAuctionRecord>
{
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<ClosedAuctionRecord>
{
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<ClosedAuctionRecord>
{
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<ClosedAuctionRecord>
{
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);
}
}
@@ -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<AuctionResultExtended>
{
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<AuctionResultExtended>();
// 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
};
}
}
@@ -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 = @"<span class=""text-muted product-value""><span class=""hidden-xs"">Valore: </span><span class=""product-value hidden-xs"">18,90 €</span></span>";
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 = @"<span class=""buyitnow-button label"">Compra a 25,50 €</span>";
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 = @"<strong class=""mobile-left-truck"">Spese di spedizione:</strong> <span class=""text-success"">4,99 €</span>";
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 = @"<strong>Spese di transazione:</strong> <span class=""text-success"">1,00 €</span>";
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: <span class=""limit-info"">1 ogni 30 giorni</span>";
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);
}
}