test: expand coverage to 209 tests across all core modules
Added tests for CoinAmount (all units, comma decimal, sub-satoshi rejection, overflow, unknown unit ArgumentException, roundtrip), Storage (nonce/salt uniqueness per encrypt, atomic write, WalletLock acquire/release/stale-file), WalletLoader (NewFromMnemonic, passphrase isolation, watch-only, invalid inputs, ProfileOf), SPV/Merkle (single tx, odd/even counts, altered proof, empty list), and Chain (unknown NetKind, distinct profiles, port constants).
This commit is contained in:
@@ -1,39 +1,137 @@
|
||||
using System;
|
||||
using PalladiumWallet.Core.Wallet;
|
||||
|
||||
namespace PalladiumWallet.Tests.Wallet;
|
||||
|
||||
public class CoinAmountTests
|
||||
{
|
||||
// --- importi validi ---
|
||||
// ---- importi validi: tutte le unità ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("1", "sat", 1)]
|
||||
[InlineData("0", "sat", 0)]
|
||||
[InlineData("1.5", "PLM", 150_000_000)]
|
||||
[InlineData("0.00000001", "PLM", 1)]
|
||||
[InlineData("1", "PLM", 100_000_000)]
|
||||
[InlineData("1.00000", "mPLM", 100_000)]
|
||||
[InlineData("1.00", "µPLM", 100)]
|
||||
[InlineData("1", "sat", 1)]
|
||||
[InlineData("0", "sat", 0)]
|
||||
[InlineData("0", "PLM", 0)]
|
||||
[InlineData("0", "mPLM", 0)]
|
||||
[InlineData("0", "µPLM", 0)]
|
||||
[InlineData("1.5", "PLM", 150_000_000)]
|
||||
[InlineData("0.00000001", "PLM", 1)]
|
||||
[InlineData("1", "PLM", 100_000_000)]
|
||||
[InlineData("1.00000", "mPLM", 100_000)]
|
||||
[InlineData("0.00001", "mPLM", 1)]
|
||||
[InlineData("1.00", "µPLM", 100)]
|
||||
[InlineData("0.01", "µPLM", 1)]
|
||||
[InlineData("100000000", "sat", 100_000_000)]
|
||||
public void Importo_valido_viene_accettato(string input, string unit, long expectedSats)
|
||||
{
|
||||
Assert.True(CoinAmount.TryParseIn(input, unit, out var sats));
|
||||
Assert.Equal(expectedSats, sats);
|
||||
}
|
||||
|
||||
// --- importi con troppi decimali: devono essere rifiutati ---
|
||||
// ---- decimale con virgola (locale italiano) ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("1.9", "sat")] // sat non è divisibile
|
||||
[InlineData("0.001", "µPLM")] // 0.001 × 100 = 0.1 sat
|
||||
[InlineData("1.500000001", "PLM")] // 9 decimali, uno di troppo
|
||||
[InlineData("0.000000001", "PLM")] // sotto al satoshi
|
||||
[InlineData("1.1", "sat")]
|
||||
[InlineData("1,5", "PLM", 150_000_000)]
|
||||
[InlineData("1,00000", "mPLM", 100_000)]
|
||||
[InlineData("0,00000001", "PLM", 1)]
|
||||
public void Virgola_italiana_viene_accettata(string input, string unit, long expectedSats)
|
||||
{
|
||||
Assert.True(CoinAmount.TryParseIn(input, unit, out var sats));
|
||||
Assert.Equal(expectedSats, sats);
|
||||
}
|
||||
|
||||
// ---- spazi iniziali/finali ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(" 1 ", "sat", 1)]
|
||||
[InlineData(" 1.5 ", "PLM", 150_000_000)]
|
||||
public void Spazi_iniziali_e_finali_vengono_ignorati(string input, string unit, long expectedSats)
|
||||
{
|
||||
Assert.True(CoinAmount.TryParseIn(input, unit, out var sats));
|
||||
Assert.Equal(expectedSats, sats);
|
||||
}
|
||||
|
||||
// ---- importi con troppi decimali ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("1.9", "sat")]
|
||||
[InlineData("1.1", "sat")]
|
||||
[InlineData("0.001", "µPLM")]
|
||||
[InlineData("1.500000001", "PLM")]
|
||||
[InlineData("0.000000001", "PLM")]
|
||||
[InlineData("0.000001", "mPLM")]
|
||||
public void Importo_con_troppi_decimali_viene_rifiutato(string input, string unit)
|
||||
{
|
||||
Assert.False(CoinAmount.TryParseIn(input, unit, out _));
|
||||
}
|
||||
|
||||
// --- TryParseCoins ---
|
||||
// ---- negativi ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("-1", "sat")]
|
||||
[InlineData("-0.1", "PLM")]
|
||||
[InlineData("-1", "PLM")]
|
||||
public void Importo_negativo_viene_rifiutato(string input, string unit)
|
||||
{
|
||||
Assert.False(CoinAmount.TryParseIn(input, unit, out _));
|
||||
}
|
||||
|
||||
// ---- stringa vuota e non numerica ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("abc")]
|
||||
[InlineData("1e5")]
|
||||
[InlineData("∞")]
|
||||
public void Stringa_non_numerica_viene_rifiutata(string input)
|
||||
{
|
||||
Assert.False(CoinAmount.TryParseIn(input, "PLM", out _));
|
||||
}
|
||||
|
||||
// ---- overflow ----
|
||||
|
||||
[Fact]
|
||||
public void Overflow_viene_rifiutato()
|
||||
{
|
||||
// 92233720368.54775807 PLM supera long.MaxValue in satoshi
|
||||
Assert.False(CoinAmount.TryParseIn("99999999999", "PLM", out _));
|
||||
}
|
||||
|
||||
// ---- unità sconosciuta lancia ArgumentException ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("banana")]
|
||||
[InlineData("BTC")]
|
||||
[InlineData("")]
|
||||
[InlineData("plm")] // case-sensitive
|
||||
public void Unita_sconosciuta_lancia_ArgumentException(string unit)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => CoinAmount.TryParseIn("1", unit, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatIn_unita_sconosciuta_lancia_ArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => CoinAmount.FormatIn(1, "banana"));
|
||||
}
|
||||
|
||||
// ---- roundtrip FormatIn → TryParseIn ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, "PLM")]
|
||||
[InlineData(1, "sat")]
|
||||
[InlineData(1, "PLM")]
|
||||
[InlineData(150_000_000, "PLM")]
|
||||
[InlineData(100_000, "mPLM")]
|
||||
[InlineData(100, "µPLM")]
|
||||
[InlineData(99_999_999, "PLM")]
|
||||
public void Roundtrip_format_parse_conserva_i_satoshi(long sats, string unit)
|
||||
{
|
||||
var formatted = CoinAmount.FormatIn(sats, unit, withLabel: false);
|
||||
Assert.True(CoinAmount.TryParseIn(formatted, unit, out var parsed));
|
||||
Assert.Equal(sats, parsed);
|
||||
}
|
||||
|
||||
// ---- TryParseCoins ----
|
||||
|
||||
[Fact]
|
||||
public void TryParseCoins_accetta_precisione_massima()
|
||||
@@ -42,6 +140,13 @@ public class CoinAmountTests
|
||||
Assert.Equal(1L, sats);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseCoins_virgola_italiana()
|
||||
{
|
||||
Assert.True(CoinAmount.TryParseCoins("1,5", out var sats));
|
||||
Assert.Equal(150_000_000L, sats);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseCoins_rifiuta_sotto_al_satoshi()
|
||||
{
|
||||
@@ -53,4 +158,21 @@ public class CoinAmountTests
|
||||
{
|
||||
Assert.False(CoinAmount.TryParseCoins("-1", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseCoins_rifiuta_overflow()
|
||||
{
|
||||
Assert.False(CoinAmount.TryParseCoins("99999999999", out _));
|
||||
}
|
||||
|
||||
// ---- Format (PLM con 8 decimali) ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(100_000_000, "1.00000000")]
|
||||
[InlineData(1, "0.00000001")]
|
||||
[InlineData(0, "0.00000000")]
|
||||
public void Format_produce_stringa_con_8_decimali(long sats, string expected)
|
||||
{
|
||||
Assert.Equal(expected, CoinAmount.Format(sats));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using PalladiumWallet.Core.Chain;
|
||||
using PalladiumWallet.Core.Crypto;
|
||||
using PalladiumWallet.Core.Storage;
|
||||
using PalladiumWallet.Core.Wallet;
|
||||
|
||||
namespace PalladiumWallet.Tests.Wallet;
|
||||
|
||||
public class WalletLoaderTests
|
||||
{
|
||||
private const string ValidMnemonic =
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
||||
|
||||
private const string ValidMnemonic24 =
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon " +
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
|
||||
|
||||
// ---- NewFromMnemonic ----
|
||||
|
||||
[Fact]
|
||||
public void NewFromMnemonic_crea_documento_con_rete_e_tipo_corretti()
|
||||
{
|
||||
var profile = ChainProfiles.Mainnet;
|
||||
var (doc, account) = WalletLoader.NewFromMnemonic(
|
||||
ValidMnemonic, passphrase: null, ScriptKind.NativeSegwit, profile);
|
||||
|
||||
Assert.Equal("mainnet", doc.Network);
|
||||
Assert.Equal("NativeSegwit", doc.ScriptKind);
|
||||
Assert.Equal(ValidMnemonic, doc.Mnemonic);
|
||||
Assert.Null(doc.Passphrase);
|
||||
Assert.NotEmpty(doc.AccountXpub);
|
||||
Assert.NotEmpty(doc.MasterFingerprint);
|
||||
Assert.False(doc.IsWatchOnly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewFromMnemonic_stessa_mnemonica_produce_stesso_xpub()
|
||||
{
|
||||
var profile = ChainProfiles.Mainnet;
|
||||
var (doc1, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, null, ScriptKind.NativeSegwit, profile);
|
||||
var (doc2, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, null, ScriptKind.NativeSegwit, profile);
|
||||
|
||||
Assert.Equal(doc1.AccountXpub, doc2.AccountXpub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewFromMnemonic_passphrase_diversa_produce_xpub_diverso()
|
||||
{
|
||||
var profile = ChainProfiles.Mainnet;
|
||||
var (doc1, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, null, ScriptKind.NativeSegwit, profile);
|
||||
var (doc2, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, "passphrase", ScriptKind.NativeSegwit, profile);
|
||||
|
||||
Assert.NotEqual(doc1.AccountXpub, doc2.AccountXpub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewFromMnemonic_mnemonica_invalida_lancia_eccezione()
|
||||
{
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
WalletLoader.NewFromMnemonic("parole non valide foo bar", null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewFromMnemonic_mnemonica_24_parole_funziona()
|
||||
{
|
||||
var (doc, _) = WalletLoader.NewFromMnemonic(
|
||||
ValidMnemonic24, null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
Assert.Equal("mainnet", doc.Network);
|
||||
Assert.NotEmpty(doc.AccountXpub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewFromMnemonic_tipi_script_producono_xpub_diversi()
|
||||
{
|
||||
var profile = ChainProfiles.Mainnet;
|
||||
var (docNative, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, null, ScriptKind.NativeSegwit, profile);
|
||||
var (docWrapped, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, null, ScriptKind.WrappedSegwit, profile);
|
||||
var (docLegacy, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, null, ScriptKind.Legacy, profile);
|
||||
|
||||
Assert.NotEqual(docNative.AccountXpub, docWrapped.AccountXpub);
|
||||
Assert.NotEqual(docNative.AccountXpub, docLegacy.AccountXpub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewFromMnemonic_reti_diverse_producono_xpub_diverse()
|
||||
{
|
||||
var (docMain, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
var (docTest, _) = WalletLoader.NewFromMnemonic(ValidMnemonic, null, ScriptKind.NativeSegwit, ChainProfiles.Testnet);
|
||||
|
||||
Assert.NotEqual(docMain.AccountXpub, docTest.AccountXpub);
|
||||
}
|
||||
|
||||
// ---- ToAccount ----
|
||||
|
||||
[Fact]
|
||||
public void ToAccount_da_mnemonica_deriva_gli_stessi_indirizzi_ad_ogni_caricamento()
|
||||
{
|
||||
var (doc, _) = WalletLoader.NewFromMnemonic(
|
||||
ValidMnemonic, null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
var account1 = WalletLoader.ToAccount(doc);
|
||||
var account2 = WalletLoader.ToAccount(doc);
|
||||
|
||||
Assert.Equal(
|
||||
account1.GetReceiveAddress(0).ToString(),
|
||||
account2.GetReceiveAddress(0).ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToAccount_da_mnemonica_non_e_watch_only()
|
||||
{
|
||||
var (doc, _) = WalletLoader.NewFromMnemonic(
|
||||
ValidMnemonic, null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
var account = WalletLoader.ToAccount(doc);
|
||||
Assert.False(account.IsWatchOnly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToAccount_da_xpub_e_watch_only_e_produce_stessi_indirizzi()
|
||||
{
|
||||
var (doc, accountSeed) = WalletLoader.NewFromMnemonic(
|
||||
ValidMnemonic, null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
|
||||
// Crea documento watch-only rimuovendo la mnemonica
|
||||
var docWo = new WalletDocument
|
||||
{
|
||||
Network = doc.Network,
|
||||
ScriptKind = doc.ScriptKind,
|
||||
AccountPath = doc.AccountPath,
|
||||
AccountXpub = doc.AccountXpub,
|
||||
};
|
||||
|
||||
var accountWo = WalletLoader.ToAccount(docWo);
|
||||
|
||||
Assert.True(accountWo.IsWatchOnly);
|
||||
Assert.Equal(
|
||||
accountSeed.GetReceiveAddress(0).ToString(),
|
||||
accountWo.GetReceiveAddress(0).ToString());
|
||||
Assert.Equal(
|
||||
accountSeed.GetReceiveAddress(9).ToString(),
|
||||
accountWo.GetReceiveAddress(9).ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToAccount_watch_only_non_espone_chiavi_private()
|
||||
{
|
||||
var (doc, _) = WalletLoader.NewFromMnemonic(
|
||||
ValidMnemonic, null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
var docWo = new WalletDocument
|
||||
{
|
||||
Network = doc.Network,
|
||||
ScriptKind = doc.ScriptKind,
|
||||
AccountPath = doc.AccountPath,
|
||||
AccountXpub = doc.AccountXpub,
|
||||
};
|
||||
var account = WalletLoader.ToAccount(docWo);
|
||||
Assert.Throws<InvalidOperationException>(() => account.GetExtPrivateKey(false, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToAccount_rete_sconosciuta_lancia_eccezione()
|
||||
{
|
||||
var (doc, _) = WalletLoader.NewFromMnemonic(
|
||||
ValidMnemonic, null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
var docBad = new WalletDocument
|
||||
{
|
||||
Network = "fantanet",
|
||||
ScriptKind = doc.ScriptKind,
|
||||
AccountPath = doc.AccountPath,
|
||||
AccountXpub = doc.AccountXpub,
|
||||
Mnemonic = doc.Mnemonic,
|
||||
};
|
||||
Assert.ThrowsAny<Exception>(() => WalletLoader.ToAccount(docBad));
|
||||
}
|
||||
|
||||
// ---- ProfileOf ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("mainnet", NetKind.Mainnet)]
|
||||
[InlineData("testnet", NetKind.Testnet)]
|
||||
[InlineData("regtest", NetKind.Regtest)]
|
||||
[InlineData("Mainnet", NetKind.Mainnet)]
|
||||
public void ProfileOf_riconosce_le_reti_note(string network, NetKind expected)
|
||||
{
|
||||
var doc = new WalletDocument { Network = network, ScriptKind = "NativeSegwit",
|
||||
AccountPath = "84'/0'/0'", AccountXpub = "xpub" };
|
||||
Assert.Equal(expected, WalletLoader.ProfileOf(doc).Kind);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user