- 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.
233 lines
8.5 KiB
C#
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 }));
|
|
}
|
|
}
|