made-client 0.7.8

Reusable operator client for MADE's public gRPC API.
Documentation
use made_proto::v1::{CeremonyInstanceState, CeremonyLineageState};

use crate::{CeremonyTree, MadeClientError, ProgressCheckpoint, RequestContext};

#[tokio::test]
async fn empty_invocation_id_is_rejected() {
    assert!(matches!(
        RequestContext::from_id("   "),
        Err(MadeClientError::InvalidInvocationId)
    ));
}

#[tokio::test]
async fn checkpoint_round_trips_atomically_and_rejects_another_scope() {
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp");
    std::fs::create_dir_all(&root).unwrap();
    let directory = tempfile::tempdir_in(root).unwrap();
    let path = directory.path().join("nested/cursor.json");
    let checkpoint = ProgressCheckpoint::new("ceremony-a", 42);
    checkpoint.save(&path).await.unwrap();
    assert_eq!(
        ProgressCheckpoint::load(&path, "ceremony-a").await.unwrap(),
        checkpoint
    );
    assert!(matches!(
        ProgressCheckpoint::load(&path, "ceremony-b").await,
        Err(MadeClientError::CursorScopeMismatch { .. })
    ));
}

#[test]
fn tree_orders_siblings_and_rejects_a_cycle() {
    let root = instance("root", "", 0);
    let second = instance("second", "root", 2);
    let first = instance("first", "root", 1);
    let tree = CeremonyTree::from_instances(vec![second, root, first]).unwrap();
    let root = &tree.roots()[0];
    assert_eq!(root.instance().ceremony_id, "root");
    assert_eq!(root.children()[0].instance().ceremony_id, "first");
    assert_eq!(root.children()[1].instance().ceremony_id, "second");

    let cycle = CeremonyTree::from_instances(vec![
        instance("left", "right", 1),
        instance("right", "left", 1),
    ]);
    assert!(matches!(cycle, Err(MadeClientError::ProtocolViolation(_))));
}

fn instance(id: &str, parent: &str, position: u32) -> CeremonyInstanceState {
    CeremonyInstanceState {
        ceremony_id: id.to_owned(),
        lineage: Some(CeremonyLineageState {
            root_id: "root".to_owned(),
            parent_id: parent.to_owned(),
            group_id: String::new(),
            position,
            depth: u32::from(!parent.is_empty()),
            remaining_depth: 3,
        }),
        ..CeremonyInstanceState::default()
    }
}