Files
davide a46f5e6638 test(wallet): cover WalletLoader's defensive branches for corrupted files
ToAccount's InvalidDataException paths (corrupted mnemonic/xprv/xpub,
a document with no key material at all) and the equivalent guards in
NewFromXpub/NewFromXprv/NewFromWif had no coverage — these are the
checks that turn a damaged wallet file into a clear error instead of
an unhandled exception or, worse, a silently wrong account.
2026-07-07 16:25:37 +02:00

253 lines
9.0 KiB
C#

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);
// Build a watch-only document by removing the mnemonic.
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.True(account.IsWatchOnly);
Assert.Null(account.GetPrivateKey(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);
}
// ---- corrupted/incomplete documents (defensive branches of ToAccount) ----
private static WalletDocument EmptyDoc() => new()
{
Network = "mainnet",
ScriptKind = "NativeSegwit",
};
[Fact]
public void ToAccount_mnemonica_corrotta_nel_file_lancia_eccezione()
{
var doc = EmptyDoc();
doc.Mnemonic = "not a valid mnemonic at all";
var ex = Assert.Throws<InvalidDataException>(() => WalletLoader.ToAccount(doc));
Assert.Contains("mnemonic", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void ToAccount_xprv_corrotta_nel_file_lancia_eccezione()
{
var doc = EmptyDoc();
doc.AccountXprv = "zprvGarbageGarbageGarbage";
var ex = Assert.Throws<InvalidDataException>(() => WalletLoader.ToAccount(doc));
Assert.Contains("Xprv", ex.Message);
}
[Fact]
public void ToAccount_xpub_corrotta_nel_file_lancia_eccezione()
{
var doc = EmptyDoc();
doc.AccountXpub = "zpubGarbageGarbageGarbage";
var ex = Assert.Throws<InvalidDataException>(() => WalletLoader.ToAccount(doc));
Assert.Contains("Xpub", ex.Message);
}
[Fact]
public void ToAccount_documento_senza_alcuna_chiave_lancia_eccezione()
{
var ex = Assert.Throws<InvalidDataException>(() => WalletLoader.ToAccount(EmptyDoc()));
Assert.Contains("no xpub and no seed", ex.Message);
}
[Fact]
public void NewFromXpub_chiave_invalida_lancia_eccezione()
{
Assert.Throws<InvalidDataException>(
() => WalletLoader.NewFromXpub("zpubGarbage", ChainProfiles.Mainnet));
}
[Fact]
public void NewFromXprv_chiave_invalida_lancia_eccezione()
{
Assert.Throws<InvalidDataException>(
() => WalletLoader.NewFromXprv("zprvGarbage", ChainProfiles.Mainnet));
}
[Fact]
public void NewFromWif_senza_chiavi_lancia_eccezione()
{
Assert.Throws<InvalidDataException>(
() => WalletLoader.NewFromWif([], ScriptKind.NativeSegwit, ChainProfiles.Mainnet));
}
}