Files
PalladiumWallet/tests/PalladiumWallet.Tests/App/MainWindowViewModelSecurityTests.cs
T
davide 530b7feaa5 test(app): add headless ViewModel, CLI and localization tests
ViewModels run on Avalonia.Headless: wizard flows, password and file-lock
handling, seed and private-key reveal gating, contacts, settings, sync and
send against the fake server. The CLI entry point is invoked in-process
end to end. Localization checks cover the 7 languages, placeholders and
keys used in sources. Internal seams (UpdateCheck, ProfileOverride) keep
the tests offline.
2026-11-03 11:45:43 +01:00

213 lines
8.0 KiB
C#

using PalladiumWallet.App.Localization;
using PalladiumWallet.App.ViewModels;
using PalladiumWallet.Core.Chain;
using PalladiumWallet.Core.Crypto;
using PalladiumWallet.Core.Storage;
using PalladiumWallet.Core.Wallet;
namespace PalladiumWallet.Tests.App;
/// <summary>
/// Secret-handling gates in the UI layer: the seed and private keys must stay hidden until the
/// wallet password is supplied, and must be hidden again on close. Also settings, wallet info
/// and the remaining wizard flows.
/// </summary>
[Collection("AppPaths")]
public sealed class MainWindowViewModelSecurityTests : VmTestBase
{
[Fact]
public Task Seed_StaysHidden_UntilTheRightPasswordIsGiven() => UiSession.Run(() =>
{
var vm = new MainWindowViewModel();
RestoreFlow(vm, Mnemonic, "pw", "s1");
vm.OpenWalletInfoCommand.Execute(null);
Assert.True(vm.WalletInfoHasSeed);
Assert.True(vm.WalletInfoSeedNeedsPassword);
Assert.Equal("", vm.WalletInfoSeedText);
vm.WalletInfoSeedPasswordInput = "nope";
vm.RevealSeedCommand.Execute(null);
Assert.False(vm.IsWalletInfoSeedRevealed);
Assert.Equal(Loc.Tr("msg.wrongpassword"), vm.WalletInfoSeedError);
Assert.Equal("", vm.WalletInfoSeedText);
vm.WalletInfoSeedPasswordInput = "pw";
vm.RevealSeedCommand.Execute(null);
Assert.True(vm.IsWalletInfoSeedRevealed);
Assert.Equal(Mnemonic, vm.WalletInfoSeedText);
vm.CloseWalletInfoCommand.Execute(null); // closing wipes the revealed state
Assert.False(vm.IsWalletInfoSeedRevealed);
Assert.Equal("", vm.WalletInfoSeedText);
});
[Fact]
public Task WatchOnlyWallet_HasNoSeedToReveal() => UiSession.Run(() =>
{
var vm = new MainWindowViewModel();
var address = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet)
.GetReceiveAddress(0).ToString();
vm.WizardStartImportAddressCommand.Execute(null);
vm.ImportAddressInput = address;
vm.WizardNextFromImportAddressCommand.Execute(null);
vm.EncryptWallet = false;
vm.CreateOrRestoreCommand.Execute(null);
vm.OpenWalletInfoCommand.Execute(null);
Assert.False(vm.WalletInfoHasSeed);
vm.RevealSeedCommand.Execute(null);
Assert.False(vm.IsWalletInfoSeedRevealed);
});
[Fact]
public Task PrivateKey_IsRevealedOnlyAfterThePassword_AndOnlyWhenTheWalletHasKeys() => UiSession.Run(async () =>
{
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
WireFundedChain(account.GetReceiveAddress(0), 1_000_000);
var vm = new MainWindowViewModel();
RestoreFlow(vm, Mnemonic, "pw", "pk");
await Eventually(() => !vm.IsSyncing && vm.Addresses.Count > 0);
var row = vm.Addresses.First(a => !a.IsChange && a.Indice == 0);
Assert.True(row.HasPrivKey);
vm.ShowAddressInfo(row);
vm.RequestPrivKeyRevealCommand.Execute(null);
Assert.True(vm.IsPrivKeyPromptOpen);
Assert.False(vm.IsAddressPrivKeyRevealed);
vm.PrivKeyPromptPassword = "wrong";
vm.ConfirmPrivKeyPasswordCommand.Execute(null);
Assert.False(vm.IsAddressPrivKeyRevealed);
Assert.Equal(Loc.Tr("msg.wrongpassword"), vm.PrivKeyPromptError);
vm.PrivKeyPromptPassword = "pw";
vm.ConfirmPrivKeyPasswordCommand.Execute(null);
Assert.True(vm.IsAddressPrivKeyRevealed);
Assert.False(vm.IsPrivKeyPromptOpen);
vm.CloseAddressInfoCommand.Execute(null); // closing the sheet re-hides everything
vm.ShowAddressInfo(row);
Assert.False(vm.IsAddressPrivKeyRevealed);
});
[Fact]
public Task WatchOnlyAddressRows_NeverCarryAPrivateKey() => UiSession.Run(async () =>
{
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
WireFundedChain(account.GetReceiveAddress(0), 1_000_000);
var vm = new MainWindowViewModel();
vm.WizardStartImportXkeyCommand.Execute(null);
vm.ImportXkeyInput = account.ToSlip132();
vm.WizardNextFromImportXkeyCommand.Execute(null);
vm.EncryptWallet = false;
vm.CreateOrRestoreCommand.Execute(null);
await Eventually(() => !vm.IsSyncing && vm.Addresses.Count > 0);
Assert.All(vm.Addresses, a => Assert.False(a.HasPrivKey));
vm.ShowAddressInfo(vm.Addresses[0]);
vm.RequestPrivKeyRevealCommand.Execute(null);
Assert.False(vm.IsAddressPrivKeyRevealed);
Assert.False(vm.IsPrivKeyPromptOpen);
});
[Fact]
public Task Units_AndLanguages_AreAppliedAndPersisted() => UiSession.Run(() =>
{
var vm = new MainWindowViewModel();
vm.SetUnitCommand.Execute("sat");
Assert.True(vm.IsUnitSat);
Assert.Equal("sat", vm.UnitLabel);
Assert.Equal("sat", AppConfig.Load(AppPaths.ConfigPath()).Unit);
foreach (var lang in Loc.Languages)
{
vm.SetLanguageCommand.Execute(lang);
Assert.Equal(lang, vm.Loc.Language);
}
Assert.True(vm.IsLangZh);
Assert.False(vm.IsLangIt);
});
[Fact]
public Task SettingsFromDisk_AreRestoredByANewViewModel() => UiSession.Run(() =>
{
new MainWindowViewModel().SetLanguageCommand.Execute("de");
var vm2 = new MainWindowViewModel();
Assert.Equal("de", vm2.Loc.Language);
Assert.True(vm2.IsLangDe);
});
[Fact]
public Task Wizard_BackNavigation_ReturnsToThePreviousStep() => UiSession.Run(() =>
{
var vm = new MainWindowViewModel();
vm.WizardStartRestoreCommand.Execute(null);
vm.MnemonicInput = Mnemonic;
vm.WizardNextFromWordsCommand.Execute(null);
Assert.True(vm.IsStepPassphrase);
vm.WizardBackCommand.Execute(null);
Assert.True(vm.IsStepWords);
vm.WizardBackCommand.Execute(null);
Assert.True(vm.IsStepStart);
});
[Fact]
public Task ImportWif_CreatesAnImportedKeyWallet_AndRejectsGarbage() => UiSession.Run(() =>
{
var key = new NBitcoin.Key();
var wif = key.GetWif(PalladiumNetworks.Mainnet).ToString();
var vm = new MainWindowViewModel();
vm.WizardStartImportWifCommand.Execute(null);
vm.ImportWifInput = "definitely-not-a-wif";
vm.WizardNextFromImportWifCommand.Execute(null);
Assert.True(vm.IsStepImportWif);
vm.ImportWifInput = wif;
vm.WizardNextFromImportWifCommand.Execute(null);
vm.EncryptWallet = true;
vm.PasswordInput = vm.ConfirmPasswordInput = "pw";
vm.CreateOrRestoreCommand.Execute(null);
Assert.True(vm.IsWalletOpen);
Assert.Contains("imported", vm.NetworkInfo);
var stored = WalletStore.Load(Assert.Single(AppPaths.WalletFiles(NetKind.Mainnet)), "pw");
Assert.Equal(wif, Assert.Single(stored.WifKeys!));
});
[Fact]
public Task ImportXprv_CreatesASpendableWallet() => UiSession.Run(() =>
{
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
var vm = new MainWindowViewModel();
vm.WizardStartImportXkeyCommand.Execute(null);
vm.ImportXkeyInput = account.ToSlip132Private();
vm.WizardNextFromImportXkeyCommand.Execute(null);
vm.EncryptWallet = false;
vm.CreateOrRestoreCommand.Execute(null);
Assert.True(vm.IsWalletOpen);
Assert.False(vm.IsWatchOnlyAccount);
});
[Fact]
public Task ClosingTheWallet_ReleasesTheLock_SoItCanBeReopened() => UiSession.Run(() =>
{
var vm = new MainWindowViewModel();
RestoreFlow(vm, Mnemonic, "pw", "relock");
var path = Assert.Single(AppPaths.WalletFiles(NetKind.Mainnet));
Assert.Null(WalletLock.TryAcquire(path)); // held while open
vm.CloseWalletCommand.Execute(null);
using var l = WalletLock.TryAcquire(path); // released on close
Assert.NotNull(l);
Assert.False(vm.IsWalletOpen);
});
}