Due difetti che i registri di tre giorni mostravano chiaramente. Il riscatto dei buoni giudicava la risposta del sito, e la giudicava male: ogni singolo tentativo risultava "sessione scaduta: risposta la pagina di accesso" — 6 trovati, 0 riscossi, 6 non riusciti, giro dopo giro per tre giorni — mentre le puntate arrivavano lo stesso. Il riconoscimento della pagina di accesso scattava su pagine che di accesso non erano, e il servizio dichiarava di non funzionare mentre funzionava. Adesso aprire il collegamento e' il riscatto: si apre, si scrive che e' stato aperto, e il numero vero lo da' la differenza di saldo, che e' l'unica misura che non mente. Resta un solo fallimento possibile, l'errore di trasporto, dove il collegamento non e' stato aperto affatto. Un premio si riscuote una volta sola: fra i due errori, insistere su un premio gia' preso non costa niente, mentre dichiarare falliti dei riscatti riusciti riempie il registro di allarmi inutili. Il giudizio vecchio non e' stato cancellato, e' in GiudicaRisposta. Il conto alla rovescia in griglia restava fermo fra un poll e l'altro. Il valore era gia' giusto — si ricava dalla scadenza ancorata all'orologio del server, non dall'ultimo poll — ma nessuno lo richiedeva finche' non arrivava una risposta, e nelle prove reali fra un poll e l'altro passava anche un secondo e mezzo: sembrava un'applicazione bloccata. Un battito da cinque volte al secondo notifica ora le tre sole proprieta' che dipendono dal tempo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
854 lines
30 KiB
C#
854 lines
30 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using AutoBidder.Models;
|
|
using AutoBidder.Services;
|
|
using AutoBidder.Utilities;
|
|
using Xunit;
|
|
|
|
namespace AutoBidder.Tests;
|
|
|
|
/// <summary>
|
|
/// Il riscatto automatico gira da solo, in sottofondo: nessuno lo guarda mentre lavora.
|
|
/// La proprietà da difendere è quindi che sappia dire quando <b>non</b> ha capito la
|
|
/// pagina, invece di riportare "nessun premio" — che è indistinguibile da una giornata
|
|
/// senza ricompense e farebbe passare inosservato un cambio di formato del sito.
|
|
/// </summary>
|
|
public class FreeBidsPageParserTests
|
|
{
|
|
private const string Origin = "https://it.bidoo.com";
|
|
|
|
/// <summary>
|
|
/// Pagina abbastanza lunga da superare il controllo di lunghezza minima. Il contorno è
|
|
/// deliberatamente neutro: ogni parola che fa riconoscere la pagina deve arrivare dal
|
|
/// corpo passato dal test, altrimenti sarebbe l'impalcatura a far passare la prova.
|
|
/// </summary>
|
|
private static string Page(string body) =>
|
|
"<html><head><title>Bidoo</title></head><body>" +
|
|
new string(' ', 250) + body + "</body></html>";
|
|
|
|
[Fact]
|
|
public void Una_risposta_vuota_non_e_una_pagina_senza_premi()
|
|
{
|
|
var scan = FreeBidsPageParser.Scan("", Origin);
|
|
|
|
Assert.False(scan.PageRecognised);
|
|
Assert.Empty(scan.Claimable);
|
|
}
|
|
|
|
[Fact]
|
|
public void La_pagina_di_accesso_viene_riconosciuta_come_sessione_scaduta()
|
|
{
|
|
var scan = FreeBidsPageParser.Scan(
|
|
Page("<form action=\"/login.php\"><input name=\"password\" type=\"password\"></form>"), Origin);
|
|
|
|
Assert.False(scan.PageRecognised);
|
|
Assert.Contains("sessione", scan.Diagnostic);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_pagina_estranea_non_viene_scambiata_per_le_ricompense()
|
|
{
|
|
var scan = FreeBidsPageParser.Scan(Page("<div>Carrello vuoto</div>"), Origin);
|
|
|
|
Assert.False(scan.PageRecognised);
|
|
}
|
|
|
|
[Fact]
|
|
public void La_pagina_delle_ricompense_senza_premi_viene_capita()
|
|
{
|
|
// Questo è il caso che deve distinguersi da tutti i precedenti: pagina giusta,
|
|
// semplicemente nulla da prendere.
|
|
var scan = FreeBidsPageParser.Scan(Page("<div class=\"chest empty\">Nessun baule disponibile</div>"), Origin);
|
|
|
|
Assert.True(scan.PageRecognised);
|
|
Assert.Empty(scan.Claimable);
|
|
}
|
|
|
|
[Fact]
|
|
public void Riconosce_un_premio_da_collegamento_diretto()
|
|
{
|
|
var scan = FreeBidsPageParser.Scan(
|
|
Page("<div class=\"chest\"><a href=\"/open_chest.php?id=4821\">Apri</a></div>"), Origin);
|
|
|
|
Assert.True(scan.PageRecognised);
|
|
var reward = Assert.Single(scan.Claimable);
|
|
Assert.Equal($"{Origin}/open_chest.php?id=4821", reward.ClaimUrl);
|
|
Assert.Equal("4821", reward.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void Riconosce_un_premio_da_chiamata_javascript()
|
|
{
|
|
var scan = FreeBidsPageParser.Scan(
|
|
Page("<div class=\"baule\"><button onclick=\"openChest(77)\">Apri</button></div>"), Origin);
|
|
|
|
Assert.True(scan.PageRecognised);
|
|
Assert.Equal($"{Origin}/open_chest.php?id=77", Assert.Single(scan.Claimable).ClaimUrl);
|
|
}
|
|
|
|
[Fact]
|
|
public void Lo_stesso_premio_ripetuto_si_conta_una_volta_sola()
|
|
{
|
|
// Immagine e pulsante puntano allo stesso premio: contarlo due volte gonfierebbe
|
|
// i contatori senza che sia arrivata una puntata in più.
|
|
var scan = FreeBidsPageParser.Scan(Page(
|
|
"<div class=\"chest\">" +
|
|
"<a href=\"/open_chest.php?id=9\"><img src=\"baule.png\"></a>" +
|
|
"<a href=\"/open_chest.php?id=9\">Apri</a>" +
|
|
"</div>"), Origin);
|
|
|
|
Assert.True(scan.PageRecognised);
|
|
Assert.Single(scan.Claimable);
|
|
}
|
|
|
|
[Fact]
|
|
public void Piu_premi_diversi_vengono_trovati_tutti()
|
|
{
|
|
var scan = FreeBidsPageParser.Scan(Page(
|
|
"<div class=\"chest\"><a href=\"/open_chest.php?id=1\">Apri</a></div>" +
|
|
"<div class=\"chest\"><a href=\"/open_chest.php?id=2\">Apri</a></div>"), Origin);
|
|
|
|
Assert.Equal(2, scan.Claimable.Count);
|
|
Assert.Contains(scan.Claimable, r => r.Id == "1");
|
|
Assert.Contains(scan.Claimable, r => r.Id == "2");
|
|
}
|
|
|
|
[Fact]
|
|
public void Gli_indirizzi_relativi_diventano_assoluti()
|
|
{
|
|
var scan = FreeBidsPageParser.Scan(
|
|
Page("<div class=\"chest\"><a href=\"open_chest.php?id=5\">Apri</a></div>"), Origin);
|
|
|
|
Assert.StartsWith(Origin, Assert.Single(scan.Claimable).ClaimUrl);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Complimenti! Hai ricevuto 3 puntate", 3)]
|
|
[InlineData("Hai vinto 25 bids gratis", 25)]
|
|
[InlineData("Nessun premio disponibile", null)]
|
|
public void Legge_le_puntate_dichiarate_nella_risposta(string body, int? expected)
|
|
{
|
|
Assert.Equal(expected, FreeBidsPageParser.ReadGainedBids(body));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// I riferimenti stanno su file proprio perché il sito cambia: la proprietà che conta è
|
|
/// che <b>modificare il file cambi davvero la richiesta</b>. Se il codice tenesse da parte
|
|
/// un indirizzo suo, il file diventerebbe una decorazione e la correzione fatta dall'utente
|
|
/// non avrebbe alcun effetto — un guasto particolarmente difficile da capire dall'esterno.
|
|
/// </summary>
|
|
public class FreeBidsSiteConfigTests
|
|
{
|
|
[Theory]
|
|
[InlineData("/my_chests.php", "https://it.bidoo.com/my_chests.php")]
|
|
[InlineData("my_chests.php", "https://it.bidoo.com/my_chests.php")]
|
|
[InlineData("https://altro.bidoo.com/x.php", "https://altro.bidoo.com/x.php")]
|
|
public void Un_percorso_diventa_un_indirizzo_intero(string path, string expected)
|
|
{
|
|
Assert.Equal(expected, new FreeBidsSiteConfig().Url(path));
|
|
}
|
|
|
|
[Fact]
|
|
public void Il_dominio_si_cambia_dal_file()
|
|
{
|
|
var config = new FreeBidsSiteConfig();
|
|
config.SiteSettings.BaseUrl = "https://www.bidoo.com/";
|
|
|
|
Assert.Equal("https://www.bidoo.com", config.Origin);
|
|
Assert.Equal("https://www.bidoo.com/my_chests.php", config.Url("/my_chests.php"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("https://it.bidoo.com/promo/abc", true)]
|
|
[InlineData("https://IT.BIDOO.COM/promo/abc", true)]
|
|
[InlineData("https://it-bidoo.com.altro.it/promo/abc", false)]
|
|
[InlineData("non un indirizzo", false)]
|
|
public void Riconosce_gli_indirizzi_di_casa(string url, bool expected)
|
|
{
|
|
Assert.Equal(expected, new FreeBidsSiteConfig().IsSameOrigin(url));
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_attesa_assurda_viene_riportata_entro_limiti_sensati()
|
|
{
|
|
var config = new FreeBidsSiteConfig();
|
|
|
|
config.SiteSettings.TimeoutSeconds = 0;
|
|
Assert.Equal(8, config.Timeout.TotalSeconds);
|
|
|
|
config.SiteSettings.TimeoutSeconds = 3600;
|
|
Assert.Equal(60, config.Timeout.TotalSeconds);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_sezione_dimenticata_nel_file_non_fa_saltare_il_riscatto()
|
|
{
|
|
// È il caso normale di un file scritto a mano: si corregge un indirizzo e si
|
|
// cancella per sbaglio il resto.
|
|
var config = new FreeBidsSiteConfig
|
|
{
|
|
SiteSettings = null!,
|
|
Endpoints = null!,
|
|
ClaimParameters = null!,
|
|
ResponseValidation = null!,
|
|
PageRecognition = null!
|
|
}.Normalised();
|
|
|
|
Assert.Equal(FreeBidsSiteConfig.DefaultBaseUrl, config.Origin);
|
|
Assert.Equal("/my_chests.php", config.Endpoints.Rewards);
|
|
Assert.Equal("id", config.ClaimParameters.Daily.QueryKey);
|
|
Assert.NotEmpty(config.PageRecognition.PageMarkers);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Il file dei riferimenti viene scritto a mano, quindi arriverà rotto prima o poi.
|
|
/// Quello che non deve succedere è che un errore di battitura fermi il riscatto in
|
|
/// silenzio: si torna ai predefiniti e si <b>dice</b> perché.
|
|
/// </summary>
|
|
public class FreeBidsConfigStoreTests
|
|
{
|
|
[Fact]
|
|
public void Un_file_a_pezzi_da_i_predefiniti_e_spiega_il_motivo()
|
|
{
|
|
var config = FreeBidsConfigStore.Parse("{ questo non è json", out var problem);
|
|
|
|
Assert.NotNull(problem);
|
|
Assert.Equal(FreeBidsSiteConfig.DefaultBaseUrl, config.Origin);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_file_parziale_conserva_i_predefiniti_per_il_resto()
|
|
{
|
|
var config = FreeBidsConfigStore.Parse(
|
|
"""{ "Endpoints": { "Rewards": "/nuova_pagina.php" } }""", out var problem);
|
|
|
|
Assert.Null(problem);
|
|
Assert.Equal("/nuova_pagina.php", config.Endpoints.Rewards);
|
|
Assert.Equal("/open_chest.php", config.Endpoints.ClaimDailyReward);
|
|
}
|
|
|
|
[Fact]
|
|
public void Commenti_e_virgole_di_troppo_sono_ammessi()
|
|
{
|
|
// Chi modifica il file annota perché l'ha fatto: rifiutarglielo sarebbe un modo
|
|
// gratuito di rendere la configurazione ostile.
|
|
var config = FreeBidsConfigStore.Parse(
|
|
"""
|
|
{
|
|
// indirizzo cambiato il 3 marzo
|
|
"SiteSettings": { "BaseUrl": "https://it.bidoo.com", },
|
|
}
|
|
""", out var problem);
|
|
|
|
Assert.Null(problem);
|
|
Assert.Equal("https://it.bidoo.com", config.Origin);
|
|
}
|
|
|
|
[Fact]
|
|
public void Quello_che_si_scrive_e_quello_che_si_rilegge()
|
|
{
|
|
var original = new FreeBidsSiteConfig();
|
|
original.Endpoints.ClaimPromoLink = "/promo/redeem";
|
|
original.ClaimParameters.PromoCode.QueryKey = "codice";
|
|
|
|
var reloaded = FreeBidsConfigStore.Parse(FreeBidsConfigStore.Serialize(original), out var problem);
|
|
|
|
Assert.Null(problem);
|
|
Assert.Equal("/promo/redeem", reloaded.Endpoints.ClaimPromoLink);
|
|
Assert.Equal("codice", reloaded.ClaimParameters.PromoCode.QueryKey);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// I riconoscitori vengono dal file. Questi test dimostrano che cambiarlo cambia davvero
|
|
/// cosa viene trovato: è la ragione per cui esiste, e senza queste prove sarebbe una
|
|
/// promessa non verificata.
|
|
/// </summary>
|
|
public class FreeBidsParserConfigurationTests
|
|
{
|
|
private const string Origin = "https://it.bidoo.com";
|
|
|
|
private static string Page(string body) =>
|
|
"<html><head><title>Bidoo</title></head><body>" +
|
|
new string(' ', 250) + body + "</body></html>";
|
|
|
|
[Fact]
|
|
public void Una_parola_aggiunta_al_file_fa_riconoscere_una_forma_nuova()
|
|
{
|
|
var html = Page("<div class=\"chest\"><a href=\"/prendi_premio.php?id=12\">Prendi</a></div>");
|
|
|
|
// Con i valori di fabbrica quel collegamento non è riconoscibile…
|
|
Assert.Empty(FreeBidsPageParser.Scan(html, Origin).Claimable);
|
|
|
|
// …e con una parola in più nel file sì, senza toccare il codice.
|
|
var config = new FreeBidsSiteConfig();
|
|
config.PageRecognition.ClaimLinkKeywords.Add("prendi_premio");
|
|
|
|
var reward = Assert.Single(FreeBidsPageParser.Scan(html, config).Claimable);
|
|
Assert.Equal($"{Origin}/prendi_premio.php?id=12", reward.ClaimUrl);
|
|
}
|
|
|
|
[Fact]
|
|
public void Lindirizzo_di_riscatto_dal_solo_identificativo_viene_dal_file()
|
|
{
|
|
var config = new FreeBidsSiteConfig();
|
|
config.Endpoints.ClaimDailyReward = "/ajax/riscuoti.php";
|
|
config.ClaimParameters.Daily.QueryKey = "chest";
|
|
|
|
var scan = FreeBidsPageParser.Scan(
|
|
Page("<button onclick=\"openChest(77)\">Apri</button><div class=\"baule\"></div>"), config);
|
|
|
|
Assert.Equal($"{Origin}/ajax/riscuoti.php?chest=77", Assert.Single(scan.Claimable).ClaimUrl);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_parola_con_caratteri_speciali_non_rompe_lanalisi()
|
|
{
|
|
// Il file contiene parole, non espressioni regolari: un carattere speciale scritto
|
|
// per sbaglio non deve poter far fallire l'intera lettura della pagina.
|
|
var config = new FreeBidsSiteConfig();
|
|
config.PageRecognition.ClaimLinkKeywords.Add("premio(*");
|
|
|
|
var scan = FreeBidsPageParser.Scan(
|
|
Page("<div class=\"chest\"><a href=\"/open_chest.php?id=3\">Apri</a></div>"), config);
|
|
|
|
Assert.True(scan.PageRecognised);
|
|
Assert.Single(scan.Claimable);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("<input type=\"hidden\" name=\"csrf_token\" value=\"abc123\">")]
|
|
[InlineData("<input value=\"abc123\" name=\"csrf_token\" type=\"hidden\">")]
|
|
[InlineData("<meta name=\"csrf_token\" content=\"abc123\">")]
|
|
public void Il_token_si_legge_dalla_pagina_gia_scaricata(string field)
|
|
{
|
|
var scan = FreeBidsPageParser.Scan(
|
|
Page($"<div class=\"chest\">{field}<a href=\"/open_chest.php?id=1\">Apri</a></div>"),
|
|
new FreeBidsSiteConfig());
|
|
|
|
Assert.Equal("abc123", scan.CsrfToken);
|
|
}
|
|
|
|
[Fact]
|
|
public void Senza_token_nella_pagina_non_se_ne_inventa_uno()
|
|
{
|
|
var scan = FreeBidsPageParser.Scan(
|
|
Page("<div class=\"chest\"><a href=\"/open_chest.php?id=1\">Apri</a></div>"),
|
|
new FreeBidsSiteConfig());
|
|
|
|
Assert.Null(scan.CsrfToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// La richiesta si compone dai riferimenti: è il punto in cui la configurazione diventa
|
|
/// una chiamata vera, e l'unico che si può verificare senza rete.
|
|
/// </summary>
|
|
public class FreeBidsRequestFactoryTests
|
|
{
|
|
private static ClaimableReward Reward(string id = "42") =>
|
|
new(id, "Ricompensa", $"https://it.bidoo.com/open_chest.php?id={id}");
|
|
|
|
[Fact]
|
|
public void Con_i_valori_di_fabbrica_si_apre_il_collegamento_della_pagina()
|
|
{
|
|
var plan = FreeBidsRequestFactory.ForReward(new FreeBidsSiteConfig(), Reward(), csrfToken: null);
|
|
|
|
Assert.Equal("GET", plan.Method);
|
|
Assert.Equal("https://it.bidoo.com/open_chest.php?id=42", plan.Url);
|
|
Assert.Null(plan.FormBody);
|
|
|
|
// Il Referer è la pagina da cui si sarebbe cliccato: molti siti la controllano.
|
|
Assert.Equal("https://it.bidoo.com/my_chests.php", plan.Referer);
|
|
}
|
|
|
|
[Fact]
|
|
public void Passando_a_POST_dal_file_cambia_il_corpo_non_il_codice()
|
|
{
|
|
var config = new FreeBidsSiteConfig();
|
|
config.ClaimParameters.Daily.HttpMethod = "POST";
|
|
config.ClaimParameters.Daily.FormFields = new Dictionary<string, string> { ["action"] = "claim_daily_bids" };
|
|
|
|
var plan = FreeBidsRequestFactory.ForReward(config, Reward("7"), csrfToken: "tok3n");
|
|
|
|
Assert.True(plan.IsPost);
|
|
Assert.Contains("action=claim_daily_bids", plan.FormBody);
|
|
Assert.Contains("id=7", plan.FormBody);
|
|
Assert.Contains("csrf_token=tok3n", plan.FormBody);
|
|
}
|
|
|
|
[Fact]
|
|
public void Senza_token_il_campo_non_viene_mandato_vuoto()
|
|
{
|
|
// Un campo token vuoto è il modo più sicuro di farsi rifiutare da un sito che
|
|
// quel token non lo usa affatto.
|
|
var config = new FreeBidsSiteConfig();
|
|
config.ClaimParameters.Daily.HttpMethod = "POST";
|
|
|
|
var plan = FreeBidsRequestFactory.ForReward(config, Reward(), csrfToken: null);
|
|
|
|
Assert.DoesNotContain("csrf_token", plan.FormBody);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_codice_da_solo_usa_lindirizzo_reale_del_riscatto()
|
|
{
|
|
var plan = FreeBidsRequestFactory.TryForPromo(new FreeBidsSiteConfig(), "ig240726ypb", null, out var problem);
|
|
|
|
Assert.Equal("", problem);
|
|
|
|
// Senza il "sign" Bidoo lo rifiuterà, ed è giusto così: la firma non si può
|
|
// ricostruire, e fingere il contrario nasconderebbe il motivo del rifiuto.
|
|
Assert.Equal("https://it.bidoo.com/index.php?promocode=ig240726ypb", plan!.Url);
|
|
}
|
|
|
|
[Fact]
|
|
public void Lindirizzo_del_riscatto_resta_modificabile_dal_file()
|
|
{
|
|
var config = new FreeBidsSiteConfig();
|
|
config.Endpoints.ClaimPromoLink = "/promo/redeem";
|
|
config.ClaimParameters.PromoCode.QueryKey = "code";
|
|
|
|
var plan = FreeBidsRequestFactory.TryForPromo(config, "ABC-123", null, out _);
|
|
|
|
Assert.Equal("https://it.bidoo.com/promo/redeem?code=ABC-123", plan!.Url);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_collegamento_intero_viene_seguito_esattamente_com_e()
|
|
{
|
|
// I collegamenti personali possono portare una firma: aggiungerci parametri
|
|
// sarebbe il modo più rapido per invalidarli.
|
|
var link = "https://it.bidoo.com/free-bids?code=XYZ&sig=99";
|
|
|
|
var plan = FreeBidsRequestFactory.TryForPromo(new FreeBidsSiteConfig(), link, null, out var problem);
|
|
|
|
Assert.Equal("", problem);
|
|
Assert.Equal(link, plan!.Url);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_collegamento_estraneo_non_viene_aperto()
|
|
{
|
|
// Seguirlo significherebbe mandare il cookie di sessione a un altro sito.
|
|
var plan = FreeBidsRequestFactory.TryForPromo(
|
|
new FreeBidsSiteConfig(), "https://esempio-truffa.it/regalo?code=1", null, out var problem);
|
|
|
|
Assert.Null(plan);
|
|
Assert.Contains("non appartiene", problem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Senza_indirizzo_configurato_lo_dice_invece_di_indovinare()
|
|
{
|
|
var config = new FreeBidsSiteConfig();
|
|
config.Endpoints.ClaimPromoLink = "";
|
|
|
|
var plan = FreeBidsRequestFactory.TryForPromo(config, "ABC-123", null, out var problem);
|
|
|
|
Assert.Null(plan);
|
|
Assert.Contains("ClaimPromoLink", problem);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Un riscatto rifiutato che venisse contato come riuscito gonfierebbe i contatori con
|
|
/// puntate mai arrivate, e — peggio — nasconderebbe che qualcosa non funziona più.
|
|
/// </summary>
|
|
public class FreeBidsResponseValidatorTests
|
|
{
|
|
private static readonly FreeBidsSiteConfig Config = new();
|
|
|
|
[Fact]
|
|
public void Un_errore_http_non_e_un_riscatto()
|
|
{
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 500, "");
|
|
|
|
Assert.False(verdict.Accepted);
|
|
Assert.Contains("500", verdict.Reason);
|
|
}
|
|
|
|
[Fact]
|
|
public void Il_json_del_sito_ha_lultima_parola()
|
|
{
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
|
"""{"success": true, "bids": 5, "message": "Hai riscattato 5 puntate!"}""");
|
|
|
|
Assert.True(verdict.Accepted);
|
|
Assert.Equal(5, verdict.BidsDeclared);
|
|
Assert.Equal("Hai riscattato 5 puntate!", verdict.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_rifiuto_con_200_resta_un_rifiuto()
|
|
{
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
|
"""{"success": false, "message": "Premio già riscattato"}""");
|
|
|
|
Assert.False(verdict.Accepted);
|
|
Assert.Equal("Premio già riscattato", verdict.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void Lesito_si_trova_anche_dentro_un_oggetto_annidato()
|
|
{
|
|
// Chiedere all'utente di indovinare il percorso completo sarebbe un modo per
|
|
// fargli sbagliare la configurazione.
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
|
"""{"status": 200, "data": {"success": "true", "bids": "3"}}""");
|
|
|
|
Assert.True(verdict.Accepted);
|
|
Assert.Equal(3, verdict.BidsDeclared);
|
|
}
|
|
|
|
[Fact]
|
|
public void Le_chiavi_si_cambiano_dal_file()
|
|
{
|
|
var config = new FreeBidsSiteConfig();
|
|
config.ResponseValidation.SuccessKey = "esito";
|
|
config.ResponseValidation.SuccessValue = "ok";
|
|
config.ResponseValidation.BidsCountKey = "bids_added";
|
|
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(config, 200,
|
|
"""{"esito": "ok", "bids_added": 12}""");
|
|
|
|
Assert.True(verdict.Accepted);
|
|
Assert.Equal(12, verdict.BidsDeclared);
|
|
}
|
|
|
|
[Fact]
|
|
public void La_pagina_di_accesso_non_conta_come_riscatto()
|
|
{
|
|
// È il caso peggiore: sessione scaduta, HTTP 200, e ogni giro conterebbe premi
|
|
// che non sono mai stati presi.
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
|
"<html><body><form action=\"/login.php\"><input name=\"password\"></form></body></html>");
|
|
|
|
Assert.False(verdict.Accepted);
|
|
Assert.Contains("sessione", verdict.Reason);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_pagina_intera_vale_come_accettata_e_le_puntate_si_leggono_dal_testo()
|
|
{
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
|
"<html><body><h1>Complimenti! Hai ricevuto 3 puntate</h1></body></html>");
|
|
|
|
Assert.True(verdict.Accepted);
|
|
Assert.Equal(3, verdict.BidsDeclared);
|
|
}
|
|
|
|
[Fact]
|
|
public void In_un_messaggio_breve_le_parole_di_rifiuto_contano()
|
|
{
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200, "Premio già riscattato oggi");
|
|
|
|
Assert.False(verdict.Accepted);
|
|
Assert.Contains("rifiutato", verdict.Reason);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_risposta_vuota_si_accetta_e_decide_il_saldo()
|
|
{
|
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200, "");
|
|
|
|
Assert.True(verdict.Accepted);
|
|
Assert.Null(verdict.BidsDeclared);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// La pagina di raccolta è di terzi: cambia senza preavviso ed è piena di collegamenti che
|
|
/// non c'entrano. Le due proprietà da difendere sono <b>prendere solo i riscatti veri</b> —
|
|
/// aprire un indirizzo altrui con la sessione attiva è il danno peggiore che questo modulo
|
|
/// possa fare — e <b>accorgersi quando la pagina è cambiata</b>, invece di riportare
|
|
/// tranquillamente "nessun collegamento" per sempre.
|
|
/// </summary>
|
|
public class PromoLinkParserTests
|
|
{
|
|
private static readonly FreeBidsPromoHarvest Settings = new();
|
|
|
|
private static string Page(string body) => $"<html><body>{body}</body></html>";
|
|
|
|
[Fact]
|
|
public void Prende_il_collegamento_di_riscatto()
|
|
{
|
|
var links = PromoLinkParser.Extract(Page(
|
|
"<a class=\"aste_links\" href=\"https://it.bidoo.com/index.php?promocode=ig240726ypb&sign=abc123\">Riscatta</a>"),
|
|
Settings, out var problem);
|
|
|
|
Assert.Null(problem);
|
|
var link = Assert.Single(links);
|
|
Assert.Equal("ig240726ypb", link.Code);
|
|
|
|
// L'href nell'HTML ha "&": senza decodificarlo il secondo parametro finirebbe
|
|
// dentro il nome del primo e il collegamento sarebbe scartato.
|
|
Assert.Contains("sign=abc123", link.Url);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_classe_fra_le_altre_va_bene_lo_stesso()
|
|
{
|
|
var links = PromoLinkParser.Extract(Page(
|
|
"<a href=\"https://it.bidoo.com/index.php?promocode=A1&sign=s\" class=\"btn aste_links grande\">Vai</a>"),
|
|
Settings, out _);
|
|
|
|
Assert.Single(links);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_classe_che_somiglia_non_basta()
|
|
{
|
|
// "aste_links_old" non è "aste_links": prenderlo per tale significherebbe aprire
|
|
// collegamenti che il sito ha smesso di usare.
|
|
var links = PromoLinkParser.Extract(Page(
|
|
"<a class=\"aste_links_old\" href=\"https://it.bidoo.com/index.php?promocode=A1&sign=s\">Vai</a>"),
|
|
Settings, out var problem);
|
|
|
|
Assert.Empty(links);
|
|
Assert.NotNull(problem);
|
|
}
|
|
|
|
[Theory]
|
|
// dominio estraneo: aprirlo manderebbe la sessione Bidoo a un altro sito
|
|
[InlineData("https://esempio-truffa.it/index.php?promocode=A1&sign=s")]
|
|
// percorso diverso da quello del riscatto
|
|
[InlineData("https://it.bidoo.com/auction.php?promocode=A1&sign=s")]
|
|
// manca la firma: non è un riscatto
|
|
[InlineData("https://it.bidoo.com/index.php?promocode=A1")]
|
|
// manca il codice: non ci sarebbe modo di riconoscere un doppione
|
|
[InlineData("https://it.bidoo.com/index.php?sign=s")]
|
|
public void Scarta_quello_che_non_e_un_riscatto(string href)
|
|
{
|
|
var links = PromoLinkParser.Extract(
|
|
Page($"<a class=\"aste_links\" href=\"{href}\">Vai</a>"), Settings, out _);
|
|
|
|
Assert.Empty(links);
|
|
}
|
|
|
|
[Fact]
|
|
public void Lo_stesso_codice_ripetuto_si_conta_una_volta_sola()
|
|
{
|
|
var links = PromoLinkParser.Extract(Page(
|
|
"<a class=\"aste_links\" href=\"https://it.bidoo.com/index.php?promocode=A1&sign=s\">Banner</a>" +
|
|
"<a class=\"aste_links\" href=\"https://it.bidoo.com/index.php?promocode=A1&sign=s\">Testo</a>" +
|
|
"<a class=\"aste_links\" href=\"https://it.bidoo.com/index.php?promocode=B2&sign=s\">Altro</a>"),
|
|
Settings, out _);
|
|
|
|
Assert.Equal(2, links.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_pagina_senza_piu_quei_collegamenti_lo_dice()
|
|
{
|
|
// È il caso che distingue "oggi non c'era niente" da "il sito è cambiato": senza
|
|
// questa differenza la raccolta smetterebbe di funzionare in silenzio.
|
|
var links = PromoLinkParser.Extract(
|
|
Page("<a class=\"menu\" href=\"/chi-siamo\">Chi siamo</a>"), Settings, out var problem);
|
|
|
|
Assert.Empty(links);
|
|
Assert.NotNull(problem);
|
|
Assert.Contains("aste_links", problem);
|
|
}
|
|
|
|
[Fact]
|
|
public void I_riferimenti_della_raccolta_si_cambiano_dal_file()
|
|
{
|
|
var settings = new FreeBidsPromoHarvest
|
|
{
|
|
LinkClass = "promo",
|
|
TargetDomain = "www.bidoo.com",
|
|
TargetPath = "/regalo.php",
|
|
RequiredQueryParams = new List<string> { "codice" },
|
|
CodeQueryParam = "codice"
|
|
};
|
|
|
|
var links = PromoLinkParser.Extract(Page(
|
|
"<a class=\"promo\" href=\"https://www.bidoo.com/regalo.php?codice=XY9\">Vai</a>"),
|
|
settings, out var problem);
|
|
|
|
Assert.Null(problem);
|
|
Assert.Equal("XY9", Assert.Single(links).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void I_parametri_della_query_si_leggono_decodificati()
|
|
{
|
|
var query = PromoLinkParser.ParseQuery("?promocode=a%20b&sign=x%2By&vuoto=");
|
|
|
|
Assert.Equal("a b", query["promocode"]);
|
|
Assert.Equal("x+y", query["sign"]);
|
|
Assert.Equal("", query["vuoto"]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// I collegamenti restano pubblicati per giorni: senza memoria dei codici già presi, ogni
|
|
/// giro riaprirebbe gli stessi indirizzi — decine di richieste identiche per premi che non
|
|
/// ci sono più.
|
|
/// </summary>
|
|
public class ClaimedPromoStoreTests
|
|
{
|
|
private static string NewCode() => "c" + System.Guid.NewGuid().ToString("N")[..10];
|
|
|
|
[Fact]
|
|
public void Un_codice_mai_visto_si_prova()
|
|
{
|
|
Assert.True(ClaimedPromoStore.ShouldTry(NewCode(), maxAttempts: 3));
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_codice_riscosso_non_si_riprova_mai_piu()
|
|
{
|
|
var code = NewCode();
|
|
ClaimedPromoStore.RecordOutcome(code, claimed: true, "riscosso");
|
|
|
|
Assert.False(ClaimedPromoStore.ShouldTry(code, maxAttempts: 3));
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_codice_fallito_si_riprova_ma_non_all_infinito()
|
|
{
|
|
// Un collegamento scaduto resta sulla pagina per sempre: ritentarlo a ogni giro
|
|
// sarebbe lo spreco da cui questa memoria difende.
|
|
var code = NewCode();
|
|
|
|
ClaimedPromoStore.RecordOutcome(code, claimed: false, "errore di rete");
|
|
Assert.True(ClaimedPromoStore.ShouldTry(code, maxAttempts: 3));
|
|
|
|
ClaimedPromoStore.RecordOutcome(code, claimed: false, "errore di rete");
|
|
ClaimedPromoStore.RecordOutcome(code, claimed: false, "errore di rete");
|
|
Assert.False(ClaimedPromoStore.ShouldTry(code, maxAttempts: 3));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dimenticare_tutto_rimette_in_gioco_i_codici()
|
|
{
|
|
var code = NewCode();
|
|
ClaimedPromoStore.RecordOutcome(code, claimed: true, "riscosso");
|
|
|
|
ClaimedPromoStore.Clear();
|
|
|
|
Assert.True(ClaimedPromoStore.ShouldTry(code, maxAttempts: 3));
|
|
Assert.Equal(0, ClaimedPromoStore.ClaimedCount);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// I contatori sono l'unico modo per sapere se il riscatto automatico sta davvero
|
|
/// portando qualcosa: devono restare coerenti fra i tre orizzonti che mostrano.
|
|
/// </summary>
|
|
public class FreeBidsStatsTests
|
|
{
|
|
private static void Reset() => FreeBidsStats.Clear();
|
|
|
|
[Fact]
|
|
public void Si_parte_da_zero()
|
|
{
|
|
Reset();
|
|
var s = FreeBidsStats.Read();
|
|
|
|
Assert.Equal(0, s.TotalBids);
|
|
Assert.Equal(0, s.SessionBids);
|
|
Assert.Null(s.LastClaimAt);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_riscatto_alimenta_sessione_oggi_e_totale_insieme()
|
|
{
|
|
Reset();
|
|
FreeBidsStats.RecordClaim(claimedCount: 2, bidsGained: 7);
|
|
|
|
var s = FreeBidsStats.Read();
|
|
|
|
Assert.Equal(7, s.SessionBids);
|
|
Assert.Equal(7, s.BidsToday);
|
|
Assert.Equal(7, s.TotalBids);
|
|
Assert.Equal(2, s.TotalClaimed);
|
|
Assert.NotNull(s.LastClaimAt);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_giro_a_vuoto_conta_come_controllo_ma_non_come_riscatto()
|
|
{
|
|
Reset();
|
|
FreeBidsStats.RecordCheck();
|
|
FreeBidsStats.RecordClaim(claimedCount: 0, bidsGained: 0);
|
|
|
|
var s = FreeBidsStats.Read();
|
|
|
|
Assert.Equal(1, s.TotalChecks);
|
|
Assert.Equal(0, s.TotalClaimed);
|
|
Assert.Null(s.LastClaimAt);
|
|
}
|
|
|
|
[Fact]
|
|
public void I_riscatti_si_sommano()
|
|
{
|
|
Reset();
|
|
FreeBidsStats.RecordClaim(1, 3);
|
|
FreeBidsStats.RecordClaim(1, 4);
|
|
|
|
Assert.Equal(7, FreeBidsStats.Read().TotalBids);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// La politica nuova: aprire il collegamento <b>e'</b> il riscatto, e non si giudica cosa
|
|
/// risponde il sito.
|
|
///
|
|
/// <para>Nasce da tre giorni di registri in cui ogni singolo riscatto risultava fallito con
|
|
/// "sessione scaduta: risposta la pagina di accesso" — 6 trovati, 0 riscossi, 6 non
|
|
/// riusciti, giro dopo giro — mentre le puntate arrivavano lo stesso. Un premio si riscuote
|
|
/// una volta sola: fra i due errori possibili, insistere su un premio gia' preso non costa
|
|
/// niente, mentre dichiarare falliti dei riscatti riusciti riempie il registro di allarmi
|
|
/// che non servono a nessuno.</para>
|
|
/// </summary>
|
|
public class FreeBidsPoliticaApertaTests
|
|
{
|
|
private static FreeBidsSiteConfig Config() => new FreeBidsSiteConfig().Normalised();
|
|
|
|
[Fact]
|
|
public void La_pagina_di_accesso_non_fa_piu_fallire_il_riscatto()
|
|
{
|
|
// Esattamente il caso che nei registri falliva sempre.
|
|
var login = "<html><body><form action=\"/login.php\">" +
|
|
new string('x', 500) + "<input name=\"password\"></form></body></html>";
|
|
|
|
var esito = FreeBidsResponseValidator.Validate(Config(), 200, login);
|
|
|
|
Assert.True(esito.Accepted);
|
|
Assert.Contains("aperto", esito.Reason);
|
|
}
|
|
|
|
[Fact]
|
|
public void Una_risposta_di_rifiuto_non_fa_piu_fallire_il_riscatto()
|
|
{
|
|
var esito = FreeBidsResponseValidator.Validate(Config(), 200, "{\"success\":false}");
|
|
|
|
Assert.True(esito.Accepted);
|
|
}
|
|
|
|
[Fact]
|
|
public void Un_errore_di_trasporto_resta_un_fallimento()
|
|
{
|
|
// Qui il collegamento non e' stato aperto affatto: dirlo non e' un giudizio sul
|
|
// contenuto, e riprovare ha senso.
|
|
var esito = FreeBidsResponseValidator.Validate(Config(), 500, "");
|
|
|
|
Assert.False(esito.Accepted);
|
|
Assert.Contains("500", esito.Reason);
|
|
}
|
|
|
|
[Fact]
|
|
public void Il_giudizio_vecchio_resta_disponibile()
|
|
{
|
|
// Non e' stato cancellato: serve ai test e a chi volesse riaccenderlo.
|
|
var esito = FreeBidsResponseValidator.GiudicaRisposta(Config(), 200, "{\"success\":false}");
|
|
|
|
Assert.False(esito.Accepted);
|
|
}
|
|
}
|