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:
2026-06-13 21:59:49 +02:00
parent a8d48bedad
commit ee0b73dd52
5 changed files with 624 additions and 38 deletions
@@ -1,3 +1,6 @@
using System;
using System.IO;
using System.Text.Json.Nodes;
using PalladiumWallet.Core.Storage;
namespace PalladiumWallet.Tests.Storage;
@@ -14,6 +17,11 @@ public class StorageTests
Labels = { ["txid123"] = "caffè" },
};
private static string TempPath() =>
Path.Combine(Path.GetTempPath(), $"plm-test-{Guid.NewGuid()}.wallet.json");
// ---- cifratura AES-GCM ----
[Fact]
public void La_cifratura_fa_roundtrip_con_la_password_giusta()
{
@@ -34,14 +42,43 @@ public class StorageTests
public void Un_file_manomesso_viene_rifiutato()
{
var cipher = EncryptedFile.Encrypt("contenuto", "pass");
// Corrompe un byte del ciphertext mantenendo base64 e JSON validi.
var node = System.Text.Json.Nodes.JsonNode.Parse(cipher)!;
var node = JsonNode.Parse(cipher)!;
var data = Convert.FromBase64String(node["Data"]!.GetValue<string>());
data[0] ^= 0xff;
node["Data"] = Convert.ToBase64String(data);
Assert.Throws<WrongPasswordException>(() => EncryptedFile.Decrypt(node.ToJsonString(), "pass"));
}
[Fact]
public void Ogni_encrypt_produce_nonce_diverso()
{
var c1 = JsonNode.Parse(EncryptedFile.Encrypt("x", "p"))!["Nonce"]!.GetValue<string>();
var c2 = JsonNode.Parse(EncryptedFile.Encrypt("x", "p"))!["Nonce"]!.GetValue<string>();
Assert.NotEqual(c1, c2);
}
[Fact]
public void Ogni_encrypt_produce_salt_diverso()
{
var s1 = JsonNode.Parse(EncryptedFile.Encrypt("x", "p"))!["Salt"]!.GetValue<string>();
var s2 = JsonNode.Parse(EncryptedFile.Encrypt("x", "p"))!["Salt"]!.GetValue<string>();
Assert.NotEqual(s1, s2);
}
[Fact]
public void IsEncrypted_restituisce_false_per_json_non_cifrato()
{
Assert.False(EncryptedFile.IsEncrypted("{\"Version\": 1}"));
}
[Fact]
public void IsEncrypted_restituisce_false_per_testo_non_json()
{
Assert.False(EncryptedFile.IsEncrypted("non è json"));
}
// ---- WalletDocument JSON ----
[Fact]
public void Il_documento_wallet_fa_roundtrip_json()
{
@@ -58,15 +95,36 @@ public class StorageTests
[Fact]
public void Una_versione_futura_del_file_viene_rifiutata()
{
var doc = SampleDoc();
var json = doc.ToJson().Replace("\"Version\": 1", "\"Version\": 99");
var json = SampleDoc().ToJson().Replace("\"Version\": 1", "\"Version\": 99");
Assert.Throws<InvalidDataException>(() => WalletDocument.FromJson(json));
}
[Fact]
public void Il_documento_senza_mnemonica_e_watch_only()
{
var doc = SampleDoc();
doc.Mnemonic = null;
Assert.True(WalletDocument.FromJson(doc.ToJson()).IsWatchOnly);
}
[Fact]
public void Json_corrotto_lancia_eccezione()
{
Assert.ThrowsAny<Exception>(() => WalletDocument.FromJson("{non è json valido}"));
}
[Fact]
public void Json_con_campi_mancanti_lancia_eccezione()
{
Assert.ThrowsAny<Exception>(() => WalletDocument.FromJson("{}"));
}
// ---- WalletStore ----
[Fact]
public void Il_wallet_store_salva_e_riapre_con_e_senza_password()
{
var path = Path.Combine(Path.GetTempPath(), $"plm-test-{Guid.NewGuid()}.wallet.json");
var path = TempPath();
try
{
WalletStore.Save(SampleDoc(), path);
@@ -86,17 +144,59 @@ public class StorageTests
}
[Fact]
public void Il_documento_senza_mnemonica_e_watch_only()
public void Scrittura_atomica_non_lascia_file_tmp()
{
var doc = SampleDoc();
doc.Mnemonic = null;
Assert.True(WalletDocument.FromJson(doc.ToJson()).IsWatchOnly);
var path = TempPath();
try
{
WalletStore.Save(SampleDoc(), path);
Assert.True(File.Exists(path));
Assert.False(File.Exists(path + ".tmp"));
}
finally
{
File.Delete(path);
}
}
[Fact]
public void Load_da_path_inesistente_lancia_eccezione()
{
var path = Path.Combine(Path.GetTempPath(), $"plm-noexist-{Guid.NewGuid()}.wallet.json");
Assert.Throws<FileNotFoundException>(() => WalletStore.Load(path));
}
[Fact]
public void Exists_restituisce_false_per_path_inesistente()
{
var path = Path.Combine(Path.GetTempPath(), $"plm-noexist-{Guid.NewGuid()}.wallet.json");
Assert.False(WalletStore.Exists(path));
}
[Fact]
public void Due_save_successivi_producono_nonce_diversi()
{
var path = TempPath();
try
{
WalletStore.Save(SampleDoc(), path, "password");
var n1 = JsonNode.Parse(File.ReadAllText(path))!["Nonce"]!.GetValue<string>();
WalletStore.Save(SampleDoc(), path, "password");
var n2 = JsonNode.Parse(File.ReadAllText(path))!["Nonce"]!.GetValue<string>();
Assert.NotEqual(n1, n2);
}
finally
{
File.Delete(path);
}
}
// ---- WalletLock ----
[Fact]
public void WalletLock_acquisisce_e_rilascia()
{
var path = Path.Combine(Path.GetTempPath(), $"plm-lock-{Guid.NewGuid()}.wallet.json");
var path = TempPath();
try
{
using var lock1 = WalletLock.TryAcquire(path);
@@ -104,7 +204,6 @@ public class StorageTests
}
finally
{
File.Delete(path);
File.Delete(path + ".lock");
}
}
@@ -112,18 +211,15 @@ public class StorageTests
[Fact]
public void WalletLock_seconda_istanza_restituisce_null()
{
var path = Path.Combine(Path.GetTempPath(), $"plm-lock-{Guid.NewGuid()}.wallet.json");
var path = TempPath();
try
{
using var lock1 = WalletLock.TryAcquire(path);
Assert.NotNull(lock1);
var lock2 = WalletLock.TryAcquire(path);
Assert.Null(lock2);
Assert.Null(WalletLock.TryAcquire(path));
}
finally
{
File.Delete(path);
File.Delete(path + ".lock");
}
}
@@ -131,7 +227,7 @@ public class StorageTests
[Fact]
public void WalletLock_riacquisibile_dopo_rilascio()
{
var path = Path.Combine(Path.GetTempPath(), $"plm-lock-{Guid.NewGuid()}.wallet.json");
var path = TempPath();
try
{
var lock1 = WalletLock.TryAcquire(path);
@@ -143,8 +239,37 @@ public class StorageTests
}
finally
{
File.Delete(path);
File.Delete(path + ".lock");
}
}
[Fact]
public void WalletLock_dispose_rimuove_il_file_lock()
{
var path = TempPath();
var lockPath = path + ".lock";
var lock1 = WalletLock.TryAcquire(path);
Assert.NotNull(lock1);
lock1!.Dispose();
Assert.False(File.Exists(lockPath));
}
[Fact]
public void WalletLock_file_lock_preesistente_ma_non_bloccato_viene_acquisito()
{
// Un .lock rimasto da un crash precedente (file esiste ma nessuno lo tiene)
// non deve bloccare l'apertura del wallet.
var path = TempPath();
var lockPath = path + ".lock";
try
{
File.WriteAllText(lockPath, "stale");
using var lock1 = WalletLock.TryAcquire(path);
Assert.NotNull(lock1);
}
finally
{
File.Delete(lockPath);
}
}
}