commit 69b848bdfd1513439da5bc67d12a1a47fadc427e
parent 2f9af736483e3913d97291063b047f2d9b1681fb
Author: triesap <tyson@radroots.org>
Date: Thu, 19 Feb 2026 18:57:17 +0000
tests: harden nostr ndb adapter error and concurrency paths
- add invalid author-hex rejection coverage for subscription setup
- add invalid pubkey-length coverage for profile lookup validation
- add concurrent multi-thread ingest coverage with deterministic query checks
- validate default and giftwrap-enabled test lanes with the new cases
Diffstat:
| M | nostr-ndb/src/ndb.rs | | | 91 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 91 insertions(+), 0 deletions(-)
diff --git a/nostr-ndb/src/ndb.rs b/nostr-ndb/src/ndb.rs
@@ -408,6 +408,97 @@ mod tests {
assert_eq!(profile.lud16.as_deref(), Some("alice@example.com"));
}
+ #[test]
+ fn subscribe_rejects_invalid_author_hex() {
+ let tmp_dir = TempDir::new().expect("tempdir should open");
+ let db_dir = tmp_dir.path().join("ndb");
+ let config = RadrootsNostrNdbConfig::new(&db_dir);
+ let ndb = RadrootsNostrNdb::open(config).expect("database should open");
+
+ let spec = RadrootsNostrNdbSubscriptionSpec::single(
+ RadrootsNostrNdbFilterSpec::new().with_author_hex("not-hex"),
+ );
+ let err = ndb.subscribe(&spec).expect_err("subscribe should fail");
+ assert!(matches!(
+ err,
+ RadrootsNostrNdbError::InvalidHex {
+ field: "author",
+ ..
+ }
+ ));
+ }
+
+ #[test]
+ fn profile_lookup_rejects_invalid_pubkey_length() {
+ let tmp_dir = TempDir::new().expect("tempdir should open");
+ let db_dir = tmp_dir.path().join("ndb");
+ let config = RadrootsNostrNdbConfig::new(&db_dir);
+ let ndb = RadrootsNostrNdb::open(config).expect("database should open");
+
+ let err = ndb
+ .get_profile_by_pubkey_hex("abcd")
+ .expect_err("lookup should fail");
+ assert!(matches!(
+ err,
+ RadrootsNostrNdbError::InvalidHexLength {
+ field: "pubkey",
+ ..
+ }
+ ));
+ }
+
+ #[test]
+ fn concurrent_ingest_handles_parallel_writers() {
+ let tmp_dir = TempDir::new().expect("tempdir should open");
+ let db_dir = tmp_dir.path().join("ndb");
+ let config = RadrootsNostrNdbConfig::new(&db_dir);
+ let ndb = RadrootsNostrNdb::open(config).expect("database should open");
+
+ let worker_count = 4usize;
+ let notes_per_worker = 20usize;
+ let mut handles = Vec::new();
+
+ for worker in 0..worker_count {
+ let db = ndb.clone();
+ handles.push(std::thread::spawn(move || {
+ let keys = RadrootsNostrKeys::generate();
+ for idx in 0..notes_per_worker {
+ let content = format!("parallel-{worker}-{idx}");
+ let event = RadrootsNostrEventBuilder::text_note(content.as_str())
+ .sign_with_keys(&keys)
+ .expect("event should sign");
+ db.ingest_event(&event, RadrootsNostrNdbIngestSource::client())
+ .expect("ingest should succeed");
+ }
+ }));
+ }
+
+ for handle in handles {
+ handle.join().expect("worker should complete");
+ }
+
+ let query_spec = RadrootsNostrNdbQuerySpec::text_notes(Some(512), None, 512);
+ let expected = worker_count * notes_per_worker;
+ let mut observed = 0usize;
+
+ for _ in 0..80 {
+ let notes = ndb.query_notes(&query_spec).expect("query should succeed");
+ observed = notes
+ .iter()
+ .filter(|note| note.content.starts_with("parallel-"))
+ .count();
+ if observed >= expected {
+ break;
+ }
+ std::thread::sleep(Duration::from_millis(25));
+ }
+
+ assert!(
+ observed >= expected,
+ "expected at least {expected} parallel notes, got {observed}"
+ );
+ }
+
#[cfg(feature = "giftwrap")]
#[test]
fn giftwrap_secret_key_hex_validates_length() {