Skip to content

Commit 0cabf1f

Browse files
joshpainterclaude
andcommitted
Add comprehensive test coverage across frontend and Rust crates
Implements test infrastructure and ~268 new tests covering areas that previously had zero test coverage. Frontend uses Vitest with jsdom and Testing Library; Rust tests use in-memory SQLite and standard #[test]/#[tokio::test] patterns. Frontend (170 tests): - Vitest setup with Tauri IPC mocking (invoke, events, plugins) - Pure function tests: amount conversion, addresses, hex, formatting, URLs - WalletConnect schema validation for all 17 commands - Zustand store tests with mocked bindings - Handler tests for CHIP-0002, offers, and high-level commands Rust (98 tests): - sage-keychain: encrypt/decrypt round-trips, keychain CRUD, serialization - sage-database: blocks, offers, collections, mempool, type conversion utils - sage-config: config defaults/round-trip, network inheritance, v1 migration - sage parse: all parse_* functions (asset IDs, coins, hashes, signatures) CI: adds lint and frontend test steps to build workflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0b6a05f commit 0cabf1f

29 files changed

Lines changed: 3759 additions & 13 deletions

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ jobs:
3131
- name: Prettier
3232
run: pnpm prettier:check
3333

34+
- name: Lint
35+
run: pnpm lint
36+
37+
- name: Frontend Tests
38+
run: pnpm test
39+
3440
- name: Install GTK
3541
run: sudo apt-get update && sudo apt-get install libgtk-3-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev
3642

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/sage-config/src/config.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,71 @@ impl Default for RpcConfig {
7070
}
7171
}
7272
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use super::*;
77+
78+
#[test]
79+
fn config_defaults() {
80+
let config = Config::default();
81+
assert_eq!(config.version, 2);
82+
assert_eq!(config.global.log_level, "INFO");
83+
assert!(config.global.fingerprint.is_none());
84+
assert_eq!(config.network.default_network, "mainnet");
85+
assert_eq!(config.network.target_peers, 5);
86+
assert!(config.network.discover_peers);
87+
assert!(!config.rpc.enabled);
88+
assert_eq!(config.rpc.port, 9257);
89+
}
90+
91+
#[test]
92+
fn config_toml_round_trip() {
93+
let config = Config::default();
94+
let toml_str = toml::to_string(&config).unwrap();
95+
let parsed: Config = toml::from_str(&toml_str).unwrap();
96+
assert_eq!(config, parsed);
97+
}
98+
99+
#[test]
100+
fn config_partial_deserialize() {
101+
// Only specify a few fields, rest should use defaults
102+
let toml_str = r#"
103+
version = 2
104+
105+
[global]
106+
log_level = "DEBUG"
107+
"#;
108+
let config: Config = toml::from_str(toml_str).unwrap();
109+
assert_eq!(config.global.log_level, "DEBUG");
110+
assert_eq!(config.network.default_network, "mainnet"); // default
111+
assert!(!config.rpc.enabled); // default
112+
}
113+
114+
#[test]
115+
fn config_with_fingerprint() {
116+
let toml_str = r#"
117+
version = 2
118+
119+
[global]
120+
log_level = "INFO"
121+
fingerprint = 12345
122+
123+
[network]
124+
default_network = "testnet11"
125+
target_peers = 3
126+
discover_peers = false
127+
128+
[rpc]
129+
enabled = true
130+
port = 8080
131+
"#;
132+
let config: Config = toml::from_str(toml_str).unwrap();
133+
assert_eq!(config.global.fingerprint, Some(12345));
134+
assert_eq!(config.network.default_network, "testnet11");
135+
assert_eq!(config.network.target_peers, 3);
136+
assert!(!config.network.discover_peers);
137+
assert!(config.rpc.enabled);
138+
assert_eq!(config.rpc.port, 8080);
139+
}
140+
}

crates/sage-config/src/network.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,122 @@ pub static TESTNET11: LazyLock<Network> = LazyLock::new(|| Network {
190190
additional_peer_introducers: vec!["introducer-testnet11.chia.net".to_string()],
191191
inherit: Some(InheritedNetwork::Testnet11),
192192
});
193+
194+
#[cfg(test)]
195+
mod tests {
196+
use super::*;
197+
198+
#[test]
199+
fn mainnet_defaults() {
200+
assert_eq!(MAINNET.name, "mainnet");
201+
assert_eq!(MAINNET.ticker, "XCH");
202+
assert_eq!(MAINNET.default_port, 8444);
203+
assert_eq!(MAINNET.precision, 12);
204+
assert!(MAINNET.prefix.is_none());
205+
assert!(MAINNET.network_id.is_none());
206+
assert!(MAINNET.agg_sig_me.is_none());
207+
}
208+
209+
#[test]
210+
fn testnet11_defaults() {
211+
assert_eq!(TESTNET11.name, "testnet11");
212+
assert_eq!(TESTNET11.ticker, "TXCH");
213+
assert_eq!(TESTNET11.default_port, 58444);
214+
assert_eq!(TESTNET11.precision, 12);
215+
}
216+
217+
#[test]
218+
fn prefix_fallback_to_lowercase_ticker() {
219+
assert_eq!(MAINNET.prefix(), "xch");
220+
assert_eq!(TESTNET11.prefix(), "txch");
221+
}
222+
223+
#[test]
224+
fn prefix_custom_override() {
225+
let mut network = MAINNET.clone();
226+
network.prefix = Some("custom".to_string());
227+
assert_eq!(network.prefix(), "custom");
228+
}
229+
230+
#[test]
231+
fn network_id_fallback_to_name() {
232+
assert_eq!(MAINNET.network_id(), "mainnet");
233+
assert_eq!(TESTNET11.network_id(), "testnet11");
234+
}
235+
236+
#[test]
237+
fn network_id_custom_override() {
238+
let mut network = MAINNET.clone();
239+
network.network_id = Some("custom-id".to_string());
240+
assert_eq!(network.network_id(), "custom-id");
241+
}
242+
243+
#[test]
244+
fn agg_sig_me_fallback_to_genesis_challenge() {
245+
assert_eq!(MAINNET.agg_sig_me(), MAINNET.genesis_challenge);
246+
}
247+
248+
#[test]
249+
fn agg_sig_me_custom_override() {
250+
let custom = Bytes32::new([42; 32]);
251+
let mut network = MAINNET.clone();
252+
network.agg_sig_me = Some(custom);
253+
assert_eq!(network.agg_sig_me(), custom);
254+
}
255+
256+
#[test]
257+
fn by_name_finds_mainnet() {
258+
let list = NetworkList::default();
259+
let found = list.by_name("mainnet");
260+
assert!(found.is_some());
261+
assert_eq!(found.unwrap().ticker, "XCH");
262+
}
263+
264+
#[test]
265+
fn by_name_finds_testnet11() {
266+
let list = NetworkList::default();
267+
let found = list.by_name("testnet11");
268+
assert!(found.is_some());
269+
assert_eq!(found.unwrap().ticker, "TXCH");
270+
}
271+
272+
#[test]
273+
fn by_name_returns_none_for_unknown() {
274+
let list = NetworkList::default();
275+
assert!(list.by_name("unknown_network").is_none());
276+
}
277+
278+
#[test]
279+
fn dns_introducers_inherited_from_mainnet() {
280+
let network = Network {
281+
additional_dns_introducers: Vec::new(),
282+
..MAINNET.clone()
283+
};
284+
let introducers = network.dns_introducers();
285+
assert!(!introducers.is_empty());
286+
assert!(introducers.contains(&"dns-introducer.chia.net".to_string()));
287+
}
288+
289+
#[test]
290+
fn dns_introducers_no_inheritance() {
291+
let mut network = MAINNET.clone();
292+
network.inherit = None;
293+
network.additional_dns_introducers = vec!["custom.dns".to_string()];
294+
let introducers = network.dns_introducers();
295+
assert_eq!(introducers, vec!["custom.dns".to_string()]);
296+
}
297+
298+
#[test]
299+
fn dns_introducers_merged_without_duplicates() {
300+
let network = Network {
301+
additional_dns_introducers: vec!["dns-introducer.chia.net".to_string()],
302+
..MAINNET.clone()
303+
};
304+
let introducers = network.dns_introducers();
305+
let count = introducers
306+
.iter()
307+
.filter(|i| *i == "dns-introducer.chia.net")
308+
.count();
309+
assert_eq!(count, 1, "Should not have duplicate introducers");
310+
}
311+
}

crates/sage-config/src/old.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,120 @@ pub fn migrate_networks(old: IndexMap<String, OldNetwork>) -> NetworkList {
198198
.collect(),
199199
}
200200
}
201+
202+
#[cfg(test)]
203+
mod tests {
204+
use super::*;
205+
206+
#[test]
207+
fn old_config_default_is_v1() {
208+
let old = OldConfig::default();
209+
assert!(old.is_old());
210+
}
211+
212+
#[test]
213+
fn migrate_config_basic() {
214+
let old = OldConfig::default();
215+
let (config, wallet_config) = migrate_config(old).unwrap();
216+
217+
assert_eq!(config.version, 2);
218+
assert_eq!(config.global.log_level, "INFO");
219+
assert!(config.global.fingerprint.is_none());
220+
assert_eq!(config.network.default_network, "mainnet");
221+
assert_eq!(config.network.target_peers, 5);
222+
assert!(config.network.discover_peers);
223+
assert!(!config.rpc.enabled);
224+
assert_eq!(config.rpc.port, 9257);
225+
assert!(wallet_config.wallets.is_empty());
226+
}
227+
228+
#[test]
229+
fn migrate_config_with_fingerprint_and_wallets() {
230+
let mut old = OldConfig::default();
231+
old.app.active_fingerprint = Some(12345);
232+
old.app.log_level = "DEBUG".to_string();
233+
old.rpc.run_on_startup = true;
234+
old.rpc.server_port = 8080;
235+
old.network.network_id = "testnet11".to_string();
236+
old.wallets.insert(
237+
"67890".to_string(),
238+
OldWalletConfig {
239+
name: "My Wallet".to_string(),
240+
..OldWalletConfig::default()
241+
},
242+
);
243+
244+
let (config, wallet_config) = migrate_config(old).unwrap();
245+
246+
assert_eq!(config.global.fingerprint, Some(12345));
247+
assert_eq!(config.global.log_level, "DEBUG");
248+
assert!(config.rpc.enabled);
249+
assert_eq!(config.rpc.port, 8080);
250+
assert_eq!(config.network.default_network, "testnet11");
251+
assert_eq!(wallet_config.wallets.len(), 1);
252+
assert_eq!(wallet_config.wallets[0].fingerprint, 67890);
253+
assert_eq!(wallet_config.wallets[0].name, "My Wallet");
254+
}
255+
256+
#[test]
257+
fn migrate_config_invalid_fingerprint_key() {
258+
let mut old = OldConfig::default();
259+
old.wallets.insert(
260+
"not_a_number".to_string(),
261+
OldWalletConfig::default(),
262+
);
263+
let result = migrate_config(old);
264+
assert!(result.is_err());
265+
}
266+
267+
#[test]
268+
fn migrate_networks_mainnet_inherits() {
269+
let genesis = Bytes32::new([1; 32]);
270+
let mut networks = IndexMap::new();
271+
networks.insert(
272+
"mainnet".to_string(),
273+
OldNetwork {
274+
default_port: 8444,
275+
ticker: "XCH".to_string(),
276+
address_prefix: "xch".to_string(),
277+
precision: 12,
278+
genesis_challenge: genesis,
279+
agg_sig_me: genesis, // same as genesis → should become None
280+
dns_introducers: vec!["dns.example.com".to_string()],
281+
},
282+
);
283+
284+
let result = migrate_networks(networks);
285+
assert_eq!(result.networks.len(), 1);
286+
let net = &result.networks[0];
287+
assert_eq!(net.name, "mainnet");
288+
assert!(net.prefix.is_none()); // matches lowercase ticker
289+
assert!(net.agg_sig_me.is_none()); // matches genesis
290+
assert!(matches!(net.inherit, Some(InheritedNetwork::Mainnet)));
291+
}
292+
293+
#[test]
294+
fn migrate_networks_custom_prefix_preserved() {
295+
let genesis = Bytes32::new([2; 32]);
296+
let agg = Bytes32::new([3; 32]);
297+
let mut networks = IndexMap::new();
298+
networks.insert(
299+
"custom".to_string(),
300+
OldNetwork {
301+
default_port: 9999,
302+
ticker: "CUST".to_string(),
303+
address_prefix: "mycustom".to_string(), // doesn't match "cust"
304+
precision: 6,
305+
genesis_challenge: genesis,
306+
agg_sig_me: agg, // different from genesis
307+
dns_introducers: vec![],
308+
},
309+
);
310+
311+
let result = migrate_networks(networks);
312+
let net = &result.networks[0];
313+
assert_eq!(net.prefix, Some("mycustom".to_string()));
314+
assert_eq!(net.agg_sig_me, Some(agg));
315+
assert!(net.inherit.is_none()); // not mainnet or testnet11
316+
}
317+
}

crates/sage-database/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ sqlx = { workspace = true, features = ["sqlite"] }
2020
thiserror = { workspace = true }
2121
tracing = { workspace = true }
2222
hex = { workspace = true }
23+
24+
[dev-dependencies]
25+
anyhow = { workspace = true }
26+
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite"] }
27+
tokio = { workspace = true }

crates/sage-database/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
mod maintenance;
22
mod serialized_primitives;
33
mod tables;
4+
#[cfg(test)]
5+
pub(crate) mod test_utils;
46
mod utils;
57

68
pub use maintenance::*;

0 commit comments

Comments
 (0)