kanban-service 0.4.0

Shared service layer implementing KanbanOperations over a pluggable PersistenceStore
Documentation
use super::super::BackendFactory;
use crate::KanbanContext;
use kanban_core::{AppConfig, Edge, EdgeDirection};
use kanban_domain::{CardEdgeType, CreateCardOptions, KanbanOperations, KanbanResult};
use tempfile::TempDir;

fn add_edge(ctx: &KanbanContext, edge: Edge<CardEdgeType>) {
    let mut graph = ctx.data_store().get_graph().unwrap();
    graph.cards.add_edge(edge);
    ctx.data_store().set_graph(graph).unwrap();
}

pub async fn test_blocks_edge_roundtrip(factory: &BackendFactory) -> KanbanResult<()> {
    let dir = TempDir::new().unwrap();
    let path = dir.path().join("test.store");
    let mut ctx = KanbanContext::open(factory(&path), AppConfig::default())
        .await
        .unwrap();

    let board = ctx.create_board("Board".into(), None).unwrap();
    let col = ctx.create_column(board.id, "Col".into(), None).unwrap();

    let card_a = ctx
        .create_card(
            board.id,
            col.id,
            "Blocker".into(),
            CreateCardOptions::default(),
        )
        .unwrap();
    let card_b = ctx
        .create_card(
            board.id,
            col.id,
            "Blocked".into(),
            CreateCardOptions::default(),
        )
        .unwrap();

    let now = chrono::Utc::now();
    add_edge(
        &ctx,
        Edge {
            source: card_a.id,
            target: card_b.id,
            edge_type: CardEdgeType::Blocks,
            direction: EdgeDirection::Directed,
            weight: Some(1.0_f32),
            created_at: now,
            archived_at: None,
        },
    );

    ctx.save().await.unwrap();
    let ctx = KanbanContext::open_deferred(factory(&path), AppConfig::default());

    let graph = ctx.graph()?;
    let edges = graph.cards.edges();
    assert_eq!(edges.len(), 1);
    let e = &edges[0];
    assert_eq!(e.source, card_a.id);
    assert_eq!(e.target, card_b.id);
    assert_eq!(e.edge_type, CardEdgeType::Blocks);
    assert_eq!(e.direction, EdgeDirection::Directed);
    assert!((e.weight.unwrap() - 1.0).abs() < f32::EPSILON);
    assert!(e.archived_at.is_none());
    Ok(())
}

pub async fn test_relates_to_edge_roundtrip(factory: &BackendFactory) -> KanbanResult<()> {
    let dir = TempDir::new().unwrap();
    let path = dir.path().join("test.store");
    let mut ctx = KanbanContext::open(factory(&path), AppConfig::default())
        .await
        .unwrap();

    let board = ctx.create_board("Board".into(), None).unwrap();
    let col = ctx.create_column(board.id, "Col".into(), None).unwrap();

    let card_a = ctx
        .create_card(board.id, col.id, "A".into(), CreateCardOptions::default())
        .unwrap();
    let card_b = ctx
        .create_card(board.id, col.id, "B".into(), CreateCardOptions::default())
        .unwrap();

    let now = chrono::Utc::now();
    add_edge(
        &ctx,
        Edge {
            source: card_a.id,
            target: card_b.id,
            edge_type: CardEdgeType::RelatesTo,
            direction: EdgeDirection::Bidirectional,
            weight: None,
            created_at: now,
            archived_at: None,
        },
    );

    ctx.save().await.unwrap();
    let ctx = KanbanContext::open_deferred(factory(&path), AppConfig::default());

    let graph = ctx.graph()?;
    let edges = graph.cards.edges();
    assert_eq!(edges.len(), 1);
    let e = &edges[0];
    assert_eq!(e.edge_type, CardEdgeType::RelatesTo);
    assert_eq!(e.direction, EdgeDirection::Bidirectional);
    assert!(e.weight.is_none());
    Ok(())
}

pub async fn test_parent_of_edge_roundtrip(factory: &BackendFactory) -> KanbanResult<()> {
    let dir = TempDir::new().unwrap();
    let path = dir.path().join("test.store");
    let mut ctx = KanbanContext::open(factory(&path), AppConfig::default())
        .await
        .unwrap();

    let board = ctx.create_board("Board".into(), None).unwrap();
    let col = ctx.create_column(board.id, "Col".into(), None).unwrap();

    let parent = ctx
        .create_card(
            board.id,
            col.id,
            "Parent".into(),
            CreateCardOptions::default(),
        )
        .unwrap();
    let child = ctx
        .create_card(
            board.id,
            col.id,
            "Child".into(),
            CreateCardOptions::default(),
        )
        .unwrap();

    let now = chrono::Utc::now();
    add_edge(
        &ctx,
        Edge {
            source: parent.id,
            target: child.id,
            edge_type: CardEdgeType::ParentOf,
            direction: EdgeDirection::Directed,
            weight: None,
            created_at: now,
            archived_at: None,
        },
    );

    ctx.save().await.unwrap();
    let ctx = KanbanContext::open_deferred(factory(&path), AppConfig::default());

    let graph = ctx.graph()?;
    let edges = graph.cards.edges();
    assert_eq!(edges.len(), 1);
    assert_eq!(edges[0].edge_type, CardEdgeType::ParentOf);
    Ok(())
}

pub async fn test_archived_edge_roundtrip(factory: &BackendFactory) -> KanbanResult<()> {
    let dir = TempDir::new().unwrap();
    let path = dir.path().join("test.store");
    let mut ctx = KanbanContext::open(factory(&path), AppConfig::default())
        .await
        .unwrap();

    let board = ctx.create_board("Board".into(), None).unwrap();
    let col = ctx.create_column(board.id, "Col".into(), None).unwrap();

    let card_a = ctx
        .create_card(board.id, col.id, "A".into(), CreateCardOptions::default())
        .unwrap();
    let card_b = ctx
        .create_card(board.id, col.id, "B".into(), CreateCardOptions::default())
        .unwrap();

    let now = chrono::Utc::now();
    add_edge(
        &ctx,
        Edge {
            source: card_a.id,
            target: card_b.id,
            edge_type: CardEdgeType::Blocks,
            direction: EdgeDirection::Directed,
            weight: Some(2.5_f32),
            created_at: now,
            archived_at: Some(now),
        },
    );

    ctx.save().await.unwrap();
    let ctx = KanbanContext::open_deferred(factory(&path), AppConfig::default());

    let graph = ctx.graph()?;
    let edges = graph.cards.edges();
    assert_eq!(edges.len(), 1);
    assert!(edges[0].archived_at.is_some());
    assert!((edges[0].weight.unwrap() - 2.5).abs() < f32::EPSILON);
    Ok(())
}

pub async fn test_multiple_edges_roundtrip(factory: &BackendFactory) -> KanbanResult<()> {
    let dir = TempDir::new().unwrap();
    let path = dir.path().join("test.store");
    let mut ctx = KanbanContext::open(factory(&path), AppConfig::default())
        .await
        .unwrap();

    let board = ctx.create_board("Board".into(), None).unwrap();
    let col = ctx.create_column(board.id, "Col".into(), None).unwrap();

    let card_a = ctx
        .create_card(board.id, col.id, "A".into(), CreateCardOptions::default())
        .unwrap();
    let card_b = ctx
        .create_card(board.id, col.id, "B".into(), CreateCardOptions::default())
        .unwrap();
    let card_c = ctx
        .create_card(board.id, col.id, "C".into(), CreateCardOptions::default())
        .unwrap();

    let now = chrono::Utc::now();
    add_edge(
        &ctx,
        Edge {
            source: card_a.id,
            target: card_b.id,
            edge_type: CardEdgeType::Blocks,
            direction: EdgeDirection::Directed,
            weight: None,
            created_at: now,
            archived_at: None,
        },
    );
    add_edge(
        &ctx,
        Edge {
            source: card_b.id,
            target: card_c.id,
            edge_type: CardEdgeType::ParentOf,
            direction: EdgeDirection::Directed,
            weight: Some(3.0_f32),
            created_at: now,
            archived_at: None,
        },
    );
    add_edge(
        &ctx,
        Edge {
            source: card_a.id,
            target: card_c.id,
            edge_type: CardEdgeType::RelatesTo,
            direction: EdgeDirection::Bidirectional,
            weight: None,
            created_at: now,
            archived_at: Some(now),
        },
    );

    ctx.save().await.unwrap();
    let ctx = KanbanContext::open_deferred(factory(&path), AppConfig::default());

    assert_eq!(ctx.graph()?.cards.edges().len(), 3);
    Ok(())
}

pub async fn test_empty_graph_roundtrip(factory: &BackendFactory) -> KanbanResult<()> {
    let dir = TempDir::new().unwrap();
    let path = dir.path().join("test.store");
    let mut ctx = KanbanContext::open(factory(&path), AppConfig::default())
        .await
        .unwrap();

    ctx.create_board("Board".into(), None).unwrap();
    ctx.save().await.unwrap();

    let ctx = KanbanContext::open_deferred(factory(&path), AppConfig::default());
    assert!(ctx.graph()?.cards.edges().is_empty());
    Ok(())
}