kcode-k1-users 0.1.0

User facade over K1 accounts and persons
Documentation
use ed25519_dalek::{Signer, SigningKey};
use kcode_k1_accounts::K1Accounts;
use kcode_k1_invites::{InviteStatus, K1Invites};
use kcode_k1_peering::K1Peering;
use kcode_k1_persons::K1Persons;
use kcode_k1_terms::{self, REVISION};
use kcode_k1_txn_ordering::K1TxnOrdering;
use kcode_k1_users::{K1Users, NewUser};
use std::path::Path;
use std::sync::{Arc, Barrier};
use tempfile::TempDir;

struct Stack {
    ordering: Arc<K1TxnOrdering>,
    invites: Arc<K1Invites>,
    accounts: Arc<K1Accounts>,
    persons: Arc<K1Persons>,
    users: Arc<K1Users>,
}

fn open(root: &Path) -> Stack {
    let ordering = Arc::new(K1TxnOrdering::open(&root.join("ordering")).unwrap());
    let peering = Arc::new(K1Peering::open(&root.join("peering"), ordering.clone()).unwrap());
    let invites = Arc::new(
        K1Invites::open(&root.join("invites"), ordering.clone(), peering.clone()).unwrap(),
    );
    let persons = Arc::new(
        K1Persons::open(&root.join("persons"), ordering.clone(), peering.clone()).unwrap(),
    );
    let accounts = Arc::new(K1Accounts::open(invites.clone()).unwrap());
    let users = Arc::new(K1Users::new(accounts.clone(), persons.clone()));
    Stack {
        ordering,
        invites,
        accounts,
        persons,
        users,
    }
}

fn credentials(seed: u8) -> ([u8; 32], [u8; 64]) {
    let key = SigningKey::from_bytes(&[seed; 32]);
    (
        key.verifying_key().to_bytes(),
        key.sign(kcode_k1_terms::text().as_bytes()).to_bytes(),
    )
}

fn new_user(username: &str, full_name: &str, seed: u8) -> NewUser {
    let (public_key, signature) = credentials(seed);
    NewUser::new(
        username,
        full_name,
        public_key,
        REVISION,
        kcode_k1_terms::sha256(),
        signature,
    )
    .unwrap()
}

#[test]
fn invalid_input_has_no_effect() {
    let root = TempDir::new().unwrap();
    let stack = open(root.path());
    let (_, code) = stack.invites.create().unwrap();
    let before = stack.ordering.tip();
    let (public_key, signature) = credentials(1);
    assert!(
        NewUser::new(
            "Alice",
            "Alice",
            public_key,
            REVISION,
            kcode_k1_terms::sha256(),
            signature,
        )
        .is_err()
    );
    assert!(
        NewUser::new(
            "alice",
            " \t",
            public_key,
            REVISION,
            kcode_k1_terms::sha256(),
            signature,
        )
        .is_err()
    );
    assert_eq!(stack.ordering.tip(), before);
    assert_eq!(stack.invites.status(&code).unwrap(), InviteStatus::Unused);
    assert!(
        stack
            .users
            .get(kcode_k1_invites::UserId::from_tx_id(
                kcode_k1_txn_ordering::TxId::from_bytes([9; 12])
            ))
            .unwrap()
            .is_none()
    );
}

#[test]
fn register_read_and_restart() {
    let root = TempDir::new().unwrap();
    let (user_id, anchor, public_key, signature) = {
        let stack = open(root.path());
        let (_, code) = stack.invites.create().unwrap();
        let (public_key, signature) = credentials(3);
        let user = stack
            .users
            .register(&code, new_user("alice", "Alice Example", 3))
            .unwrap();
        let user_id = user.user_id();
        let anchor = user.person_anchor_id();
        assert_eq!(user.username(), "alice");
        assert_eq!(user.person_id(), anchor);
        assert_eq!(user.full_name(), "Alice Example");
        assert_eq!(user.public_key(), public_key);
        assert_eq!(user.tos_revision(), REVISION);
        assert_eq!(user.tos_digest(), kcode_k1_terms::sha256());
        assert_eq!(user.acceptance_signature(), signature);
        assert_eq!(
            stack.users.get(user_id).unwrap().unwrap().person_id(),
            anchor
        );
        assert_eq!(
            stack
                .users
                .find_by_username("alice")
                .unwrap()
                .unwrap()
                .user_id(),
            user_id
        );
        assert_eq!(
            stack.accounts.get(user_id).unwrap().unwrap().person_id(),
            anchor
        );
        (user_id, anchor, public_key, signature)
    };
    let stack = open(root.path());
    let user = stack.users.get(user_id).unwrap().unwrap();
    assert_eq!(user.person_anchor_id(), anchor);
    assert_eq!(user.person_id(), anchor);
    assert_eq!(user.full_name(), "Alice Example");
    assert_eq!(user.public_key(), public_key);
    assert_eq!(user.acceptance_signature(), signature);
}

#[test]
fn retry_and_conflict_precede_effects() {
    let root = TempDir::new().unwrap();
    let stack = open(root.path());
    let (_, code) = stack.invites.create().unwrap();
    let first = stack
        .users
        .register(&code, new_user("alice", "Original", 5))
        .unwrap();
    let retry_tip = stack.ordering.tip();
    let retry = stack
        .users
        .register(&code, new_user("alice", "Ignored retry name", 5))
        .unwrap();
    assert_eq!(retry.user_id(), first.user_id());
    assert_eq!(retry.person_anchor_id(), first.person_anchor_id());
    assert_eq!(retry.full_name(), "Original");
    assert_eq!(stack.ordering.tip(), retry_tip);

    let (_, unused) = stack.invites.create().unwrap();
    let conflict_tip = stack.ordering.tip();
    assert_eq!(
        stack
            .users
            .register(&unused, new_user("alice", "Must not exist", 6))
            .err()
            .unwrap(),
        "username already registered"
    );
    assert_eq!(stack.ordering.tip(), conflict_tip);
    assert_eq!(stack.invites.status(&unused).unwrap(), InviteStatus::Unused);
}

#[test]
fn person_changes_are_resolved_transitively() {
    let root = TempDir::new().unwrap();
    let stack = open(root.path());
    let (_, code) = stack.invites.create().unwrap();
    let registered = stack
        .users
        .register(&code, new_user("alice", "Original", 7))
        .unwrap();
    let user_id = registered.user_id();
    let anchor = registered.person_anchor_id();
    stack.persons.update(anchor, "Updated".to_owned()).unwrap();
    let updated = stack.users.get(user_id).unwrap().unwrap();
    assert_eq!(updated.person_anchor_id(), anchor);
    assert_eq!(updated.person_id(), anchor);
    assert_eq!(updated.full_name(), "Updated");

    let middle = stack.persons.create("Middle".to_owned()).unwrap();
    stack.persons.resolve(middle, anchor).unwrap();
    let canonical = stack.persons.create("Canonical".to_owned()).unwrap();
    stack.persons.resolve(canonical, middle).unwrap();
    let resolved = stack.users.find_by_username("alice").unwrap().unwrap();
    assert_eq!(resolved.user_id(), user_id);
    assert_eq!(resolved.person_anchor_id(), anchor);
    assert_eq!(resolved.person_id(), canonical);
    assert_eq!(resolved.full_name(), "Canonical");
}

#[test]
fn concurrent_same_username_returns_one_user() {
    let root = TempDir::new().unwrap();
    let stack = open(root.path());
    let (_, code) = stack.invites.create().unwrap();
    let start = Arc::new(Barrier::new(5));
    let workers: Vec<_> = (0..4)
        .map(|_| {
            let users = stack.users.clone();
            let start = start.clone();
            std::thread::spawn(move || {
                start.wait();
                users.register(&code, new_user("alice", "Concurrent", 8))
            })
        })
        .collect();
    start.wait();
    let users: Vec<_> = workers
        .into_iter()
        .map(|worker| worker.join().unwrap().unwrap())
        .collect();
    let user_id = users[0].user_id();
    let anchor = users[0].person_anchor_id();
    for user in users {
        assert_eq!(user.user_id(), user_id);
        assert_eq!(user.person_anchor_id(), anchor);
        assert_eq!(user.person_id(), anchor);
    }
}