Files
Mimante/Mimante/Tests/AuctionDossierTests.cs
T
Alby96 7ca504a70a Add utility classes for theme management, waiting, watched products, and notifications
- Implement ThemeManager for dynamic light/dark theme switching in the application.
- Create Wait class for cancellable delays without exceptions for smoother user experience.
- Introduce WatchedProductsStore to manage and persist watched products in JSON format.
- Add WindowsNotifier for system notifications to inform users of important events.
- Develop ProductViewModel to encapsulate product data and manage UI interactions effectively.
2026-08-04 21:49:53 +02:00

233 lines
8.5 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using AutoBidder.Models;
using AutoBidder.Utilities;
using Xunit;
namespace AutoBidder.Tests;
/// <summary>
/// Il dossier è l'unico posto in cui certi dati esistono: Bidoo non espone né i tempi delle
/// puntate né l'andamento del prezzo di un'asta conclusa. Le proprietà da difendere sono
/// quindi tre — che il file <b>si scriva</b>, che ogni riga sia <b>JSON valido da sola</b>
/// (è quello che permette di leggerlo a pezzi e di sopravvivere a un'interruzione), e che il
/// riepilogo finale dica se la storia è completa.
/// </summary>
public class AuctionDossierTests
{
private static AppSettings Settings(bool rawPolls = true) => new()
{
WriteAuctionDossiers = true,
DossierIncludeRawPolls = rawPolls
};
private static AuctionInfo NewAuction() => new()
{
AuctionId = "test-" + Guid.NewGuid().ToString("N")[..8],
Name = "Cuffie Sony WH-1000XM5",
BidBeforeDeadlineMs = 800,
BuyNowPrice = 299,
ShippingCost = 6.90
};
private static AuctionState State(double price, double timer, int ping = 70) => new()
{
Price = price,
Timer = timer,
PollingLatencyMs = ping,
Status = AuctionStatus.Running,
LastBidder = "marco_82",
ExpiryUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (long)timer
};
private static AuctionDetailRecord Detail(AuctionInfo auction) => new()
{
AuctionId = auction.AuctionId,
Name = auction.Name,
Outcome = "Persa",
FinalPrice = 4.87,
BuyNowPrice = auction.BuyNowPrice,
ShippingCost = auction.ShippingCost,
MyBids = 9,
EndedAt = DateTime.Now,
FirstSeenAt = DateTime.Now.AddMinutes(-35),
ObservedMinutes = 35,
ConfiguredLeadMs = 800
};
private static string[] LinesOf(AuctionDossier dossier)
{
FileLogWriter.FlushAll();
return File.ReadAllLines(dossier.Path).Where(l => l.Length > 0).ToArray();
}
[Fact]
public void Aprire_il_dossier_scrive_lintestazione()
{
var auction = NewAuction();
var dossier = AuctionDossier.OpenFor(auction, Settings())!;
var header = JsonDocument.Parse(LinesOf(dossier)[0]).RootElement;
Assert.Equal("header", header.GetProperty("type").GetString());
Assert.Equal(auction.AuctionId, header.GetProperty("auctionId").GetString());
// Il valore del prodotto deve stare nel file: senza, il prezzo finale non dice
// se è stato un affare.
Assert.Equal(299, header.GetProperty("product").GetProperty("buyNowPrice").GetDouble());
// E con che anticipo la si stava seguendo: due aste con anticipi diversi non sono
// confrontabili fra loro.
Assert.Equal(800, header.GetProperty("config").GetProperty("bidBeforeDeadlineMs").GetInt32());
AuctionDossier.Abandon(auction.AuctionId, "fine test");
}
[Fact]
public void Ogni_riga_e_json_valido_per_conto_suo()
{
// È la ragione del formato: un file troncato a metà perde l'ultima riga, non tutto.
var auction = NewAuction();
var dossier = AuctionDossier.OpenFor(auction, Settings())!;
dossier.Poll(State(1.24, 15), includeRaw: true);
dossier.Reset(1, 1.25, "marco_82");
dossier.MyBid(1.25, 800, 762.4, 68, success: true, error: null, bidsUsed: 4, remainingBids: 255);
dossier.Log("info", "Status", "asta in corso");
var lines = LinesOf(dossier);
Assert.All(lines, line => JsonDocument.Parse(line));
Assert.Equal(5, lines.Length); // intestazione + quattro eventi
AuctionDossier.Abandon(auction.AuctionId, "fine test");
}
[Fact]
public void La_mia_puntata_registra_anticipo_voluto_e_ottenuto()
{
// È la coppia su cui si tara l'anticipo: da sola nessuna delle due dice nulla.
var auction = NewAuction();
var dossier = AuctionDossier.OpenFor(auction, Settings())!;
dossier.MyBid(1.25, plannedLeadMs: 800, actualLeadMs: 762.4, pingMs: 68,
success: true, error: null, bidsUsed: 4, remainingBids: 255);
var bid = JsonDocument.Parse(LinesOf(dossier)[1]).RootElement;
Assert.Equal("my_bid", bid.GetProperty("type").GetString());
Assert.Equal(800, bid.GetProperty("plannedLeadMs").GetInt32());
Assert.Equal(762.4, bid.GetProperty("actualLeadMs").GetDouble(), 1);
Assert.Equal(-37.6, bid.GetProperty("leadErrorMs").GetDouble(), 1);
Assert.Equal(68, bid.GetProperty("pingMs").GetInt32());
Assert.Equal("ok", bid.GetProperty("result").GetString());
AuctionDossier.Abandon(auction.AuctionId, "fine test");
}
[Fact]
public void Senza_poll_grezzi_il_ping_si_misura_lo_stesso()
{
// Spegnere i poll grezzi riduce la dimensione del file, non la qualità delle
// statistiche di rete: quelle si accumulano comunque.
var auction = NewAuction();
var dossier = AuctionDossier.OpenFor(auction, Settings(rawPolls: false))!;
dossier.Poll(State(1.24, 15, ping: 60), includeRaw: false);
dossier.Poll(State(1.30, 12, ping: 80), includeRaw: false);
Assert.Single(LinesOf(dossier)); // solo l'intestazione
AuctionDossier.Close(auction, Detail(auction), null);
var summary = JsonDocument.Parse(LinesOf(dossier)[^1]).RootElement;
var network = summary.GetProperty("network");
Assert.Equal(2, network.GetProperty("samples").GetInt32());
Assert.Equal(70, network.GetProperty("avgPingMs").GetDouble());
Assert.Equal(60, network.GetProperty("minPingMs").GetInt32());
Assert.Equal(80, network.GetProperty("maxPingMs").GetInt32());
}
[Fact]
public void Il_riepilogo_dice_se_la_storia_e_completa()
{
var auction = NewAuction();
auction.ObservedFromStart = true;
auction.ObservedToEnd = true;
var dossier = AuctionDossier.OpenFor(auction, Settings())!;
AuctionDossier.Close(auction, Detail(auction), null);
var summary = JsonDocument.Parse(LinesOf(dossier)[^1]).RootElement;
Assert.Equal("summary", summary.GetProperty("type").GetString());
Assert.True(summary.GetProperty("coverage").GetProperty("complete").GetBoolean());
// Costo reale se avessi vinto: prezzo + puntate + spedizione.
Assert.True(summary.GetProperty("value").GetProperty("totalCostIfWon").GetDouble() > 4.87);
}
[Fact]
public void Dopo_il_riepilogo_non_si_scrive_piu_nulla()
{
// Una riga in coda al riepilogo farebbe dubitare di tutto ciò che c'è sopra.
var auction = NewAuction();
var dossier = AuctionDossier.OpenFor(auction, Settings())!;
AuctionDossier.Close(auction, Detail(auction), null);
var before = LinesOf(dossier).Length;
dossier.Log("info", "Status", "questa riga non deve comparire");
Assert.Equal(before, LinesOf(dossier).Length);
}
[Fact]
public void Unasta_tolta_prima_della_fine_lo_dichiara()
{
// Una serie di prezzi che si interrompe sembra un'asta finita a quel prezzo: va
// detto che i dati sono incompleti.
var auction = NewAuction();
var dossier = AuctionDossier.OpenFor(auction, Settings())!;
AuctionDossier.Abandon(auction.AuctionId, "rimossa dal monitor");
var last = JsonDocument.Parse(LinesOf(dossier)[^1]).RootElement;
Assert.Equal("abandoned", last.GetProperty("type").GetString());
Assert.Contains("incompleti", last.GetProperty("note").GetString());
}
[Fact]
public void Riaprire_la_stessa_asta_riprende_lo_stesso_file()
{
// Un riavvio a metà asta non deve spezzare la storia in due file.
var auction = NewAuction();
var first = AuctionDossier.OpenFor(auction, Settings())!;
var path = first.Path;
AuctionDossier.Abandon(auction.AuctionId, "simulazione riavvio");
var second = AuctionDossier.OpenFor(auction, Settings())!;
Assert.Equal(path, second.Path);
var lines = LinesOf(second);
Assert.Equal("resumed", JsonDocument.Parse(lines[^1]).RootElement.GetProperty("type").GetString());
AuctionDossier.Abandon(auction.AuctionId, "fine test");
}
[Fact]
public void Con_i_dossier_spenti_non_si_scrive_nessun_file()
{
var auction = NewAuction();
Assert.Null(AuctionDossier.OpenFor(auction, new AppSettings { WriteAuctionDossiers = false }));
}
}