t-rust-less-lib 0.2.16

Password manager library
Documentation
use super::{open_block_store, BlockStore, RingId, StoreError};
use crate::block_store::model::Operation;
use crate::block_store::{Change, ChangeLog};
use crate::memguard::weak::ZeroingWords;
use rand::rngs::ThreadRng;
use rand::{distributions, thread_rng, Rng};
use spectral::prelude::*;
use std::sync::Arc;
use tempfile::Builder;

fn common_store_tests(store: Arc<dyn BlockStore>) {
  let mut rng = thread_rng();
  common_test_ring(store.as_ref(), &mut rng);
  common_test_index(store.as_ref(), &mut rng);
  common_test_blocks_commits(store.as_ref(), &mut rng);
}

fn sort_ring_ids(ring_ids: Vec<RingId>) -> Vec<String> {
  let mut ids: Vec<String> = ring_ids
    .into_iter()
    .map(|(id, version)| format!("{}.{}", id, version))
    .collect();
  ids.sort();
  ids
}

fn common_test_ring(store: &dyn BlockStore, rng: &mut ThreadRng) {
  let ring1a = rng
    .sample_iter(distributions::Standard)
    .take(200 * 8)
    .collect::<Vec<u8>>();
  let ring1b = rng.sample_iter(distributions::Standard).take(200).collect::<Vec<u8>>();
  let ring2a = rng
    .sample_iter(distributions::Standard)
    .take(300 * 8)
    .collect::<Vec<u8>>();
  let ring2b = rng
    .sample_iter(distributions::Standard)
    .take(300 * 8)
    .collect::<Vec<u8>>();

  assert_that!(store.list_ring_ids()).is_ok_containing(vec![]);
  assert_that!(store.store_ring("ring1", 0, &ring1a)).is_ok();
  assert_that!(store.get_ring("ring1")).is_ok_containing((0u64, ZeroingWords::from(ring1a.as_ref())));
  assert_that!(store.list_ring_ids().map(sort_ring_ids)).is_ok_containing(vec!["ring1.0".to_string()]);

  assert_that!(store.store_ring("ring1", 0, &ring1b)).is_err_containing(StoreError::Conflict(
    "Ring ring1 with version 0 already exists".to_string(),
  ));
  assert_that!(store.store_ring("ring1", 1, &ring1b)).is_ok();
  assert_that!(store.get_ring("ring1")).is_ok_containing((1u64, ZeroingWords::from(ring1b.as_ref())));

  assert_that!(store.list_ring_ids().map(sort_ring_ids)).is_ok_containing(vec!["ring1.1".to_string()]);

  assert_that!(store.store_ring("ring2", 0, &ring2a)).is_ok();
  assert_that!(store.get_ring("ring2")).is_ok_containing((0u64, ZeroingWords::from(ring2a.as_ref())));
  assert_that!(store.list_ring_ids().map(sort_ring_ids))
    .is_ok_containing(vec!["ring1.1".to_string(), "ring2.0".to_string()]);
  assert_that!(store.store_ring("ring2", 0, &ring2b)).is_err_containing(StoreError::Conflict(
    "Ring ring2 with version 0 already exists".to_string(),
  ));
  assert_that!(store.store_ring("ring2", 123, &ring2b)).is_ok();
  assert_that!(store.get_ring("ring2")).is_ok_containing((123u64, ZeroingWords::from(ring2b.as_ref())));

  assert_that!(store.list_ring_ids().map(sort_ring_ids))
    .is_ok_containing(vec!["ring1.1".to_string(), "ring2.123".to_string()]);
}

fn common_test_index(store: &dyn BlockStore, rng: &mut ThreadRng) {
  let node1 = rng
    .sample_iter(distributions::Alphanumeric)
    .map(char::from)
    .take(40)
    .collect::<String>();
  let node1_index1 = rng
    .sample_iter(distributions::Standard)
    .take(200 * 8)
    .collect::<Vec<u8>>();
  let node1_index2 = rng
    .sample_iter(distributions::Standard)
    .take(200 * 8)
    .collect::<Vec<u8>>();
  let node2 = rng
    .sample_iter(distributions::Alphanumeric)
    .map(char::from)
    .take(40)
    .collect::<String>();
  let node2_index1 = rng
    .sample_iter(distributions::Standard)
    .take(200 * 8)
    .collect::<Vec<u8>>();
  let node2_index2 = rng
    .sample_iter(distributions::Standard)
    .take(200 * 8)
    .collect::<Vec<u8>>();

  assert_that!(store.get_index(&node1)).is_ok_containing(None);
  assert_that!(store.store_index(&node1, &node1_index1)).is_ok();
  assert_that!(store.get_index(&node2)).is_ok_containing(None);
  assert_that!(store.store_index(&node2, &node2_index1)).is_ok();
  assert_that!(store.get_index(&node1)).is_ok_containing(Some(ZeroingWords::from(node1_index1.as_ref())));
  assert_that!(store.get_index(&node2)).is_ok_containing(Some(ZeroingWords::from(node2_index1.as_ref())));
  assert_that!(store.store_index(&node1, &node1_index2)).is_ok();
  assert_that!(store.store_index(&node2, &node2_index2)).is_ok();
  assert_that!(store.get_index(&node1)).is_ok_containing(Some(ZeroingWords::from(node1_index2.as_ref())));
  assert_that!(store.get_index(&node2)).is_ok_containing(Some(ZeroingWords::from(node2_index2.as_ref())));
}

fn common_test_blocks_commits(store: &dyn BlockStore, rng: &mut ThreadRng) {
  assert_that(&store.get_block("00000000000")).is_err_containing(StoreError::InvalidBlock("00000000000".to_string()));

  let block1 = rng
    .sample_iter(distributions::Standard)
    .take(200 * 8)
    .collect::<Vec<u8>>();
  let block2 = rng
    .sample_iter(distributions::Standard)
    .take(200 * 8)
    .collect::<Vec<u8>>();
  let block3 = rng
    .sample_iter(distributions::Standard)
    .take(200 * 8)
    .collect::<Vec<u8>>();

  let block1_id = store.add_block(&block1).unwrap();
  let block2_id = store.add_block(&block2).unwrap();
  let block3_id = store.add_block(&block3).unwrap();

  assert_that!(&block1_id).is_not_equal_to(&block2_id);
  assert_that!(&block1_id).is_not_equal_to(&block3_id);
  assert_that!(&block2_id).is_not_equal_to(&block3_id);

  assert_that!(store.get_block(&block1_id)).is_ok_containing(ZeroingWords::from(block1.as_ref()));
  assert_that!(store.get_block(&block2_id)).is_ok_containing(ZeroingWords::from(block2.as_ref()));
  assert_that!(store.get_block(&block3_id)).is_ok_containing(ZeroingWords::from(block3.as_ref()));

  assert_that!(store.commit(&[
    Change {
      op: Operation::Add,
      block: block1_id.clone(),
    },
    Change {
      op: Operation::Add,
      block: block2_id.clone(),
    },
  ]))
  .is_ok();

  assert_that!(store.change_logs()).is_ok_containing(vec![ChangeLog {
    node: store.node_id().to_string(),
    changes: vec![
      Change {
        op: Operation::Add,
        block: block1_id.clone(),
      },
      Change {
        op: Operation::Add,
        block: block2_id.clone(),
      },
    ],
  }]);

  assert_that(&store.commit(&[Change {
    op: Operation::Add,
    block: block2_id.clone(),
  }]))
  .is_err()
  .matches(|error| match error {
    StoreError::Conflict(_) => true,
    _ => false,
  });

  assert_that(&store.commit(&[Change {
    op: Operation::Add,
    block: block3_id.clone(),
  }]))
  .is_ok();

  assert_that(&store.change_logs()).is_ok_containing(vec![ChangeLog {
    node: store.node_id().to_string(),
    changes: vec![
      Change {
        op: Operation::Add,
        block: block1_id,
      },
      Change {
        op: Operation::Add,
        block: block2_id,
      },
      Change {
        op: Operation::Add,
        block: block3_id,
      },
    ],
  }]);
}

#[test]
fn test_local_dir_store() {
  let tempdir = Builder::new().prefix("t-rust-less-test").tempdir().unwrap();
  #[cfg(not(windows))]
  let url = format!("file://{}", tempdir.path().to_string_lossy());
  #[cfg(windows)]
  let url = format!("file:///{}", tempdir.path().to_string_lossy().replace('\\', "/"));

  let store = open_block_store(&url, "node1").unwrap();

  common_store_tests(store);
}

#[test]
fn test_memory_store() {
  let store = open_block_store("memory://", "node1").unwrap();

  common_store_tests(store);
}

#[cfg(feature = "sled")]
#[test]
fn test_sled_store() {
  let tempdir = Builder::new().prefix("t-rust-less-test").tempdir().unwrap();
  #[cfg(not(windows))]
  let url = format!("sled://{}", tempdir.path().to_string_lossy());
  #[cfg(windows)]
  let url = format!("sled:///{}", tempdir.path().to_string_lossy().replace('\\', "/"));

  let store = open_block_store(url.as_str(), "node1").unwrap();

  common_store_tests(store);
}