tandem-server 0.4.23

HTTP server for Tandem engine APIs
Documentation
use super::*;

#[tokio::test]
async fn resource_put_patch_get_and_list_roundtrip() {
    let state = test_state().await;
    let app = app_router(state.clone());

    let put_req = Request::builder()
        .method("PUT")
        .uri("/resource/project/demo/board")
        .header("content-type", "application/json")
        .body(Body::from(
            json!({
                "value": {"status":"todo","count":1},
                "updated_by": "agent-1"
            })
            .to_string(),
        ))
        .expect("put request");
    let put_resp = app.clone().oneshot(put_req).await.expect("put response");
    assert_eq!(put_resp.status(), StatusCode::OK);

    let patch_req = Request::builder()
        .method("PATCH")
        .uri("/resource/project/demo/board")
        .header("content-type", "application/json")
        .body(Body::from(
            json!({
                "value": {"count":2},
                "if_match_rev": 1,
                "updated_by": "agent-2"
            })
            .to_string(),
        ))
        .expect("patch request");
    let patch_resp = app
        .clone()
        .oneshot(patch_req)
        .await
        .expect("patch response");
    assert_eq!(patch_resp.status(), StatusCode::OK);

    let get_req = Request::builder()
        .method("GET")
        .uri("/resource/project/demo/board")
        .body(Body::empty())
        .expect("get request");
    let get_resp = app.clone().oneshot(get_req).await.expect("get response");
    assert_eq!(get_resp.status(), StatusCode::OK);
    let get_body = to_bytes(get_resp.into_body(), usize::MAX)
        .await
        .expect("get body");
    let payload: Value = serde_json::from_slice(&get_body).expect("json");
    assert_eq!(
        payload
            .get("resource")
            .and_then(|r| r.get("rev"))
            .and_then(|v| v.as_u64()),
        Some(2)
    );
    assert_eq!(
        payload
            .get("resource")
            .and_then(|r| r.get("value"))
            .and_then(|v| v.get("status"))
            .and_then(|v| v.as_str()),
        Some("todo")
    );
    assert_eq!(
        payload
            .get("resource")
            .and_then(|r| r.get("value"))
            .and_then(|v| v.get("count"))
            .and_then(|v| v.as_i64()),
        Some(2)
    );

    let list_req = Request::builder()
        .method("GET")
        .uri("/resource?prefix=project/demo")
        .body(Body::empty())
        .expect("list request");
    let list_resp = app.clone().oneshot(list_req).await.expect("list response");
    assert_eq!(list_resp.status(), StatusCode::OK);
    let list_body = to_bytes(list_resp.into_body(), usize::MAX)
        .await
        .expect("list body");
    let list_payload: Value = serde_json::from_slice(&list_body).expect("json");
    assert_eq!(list_payload.get("count").and_then(|v| v.as_u64()), Some(1));
}

#[tokio::test]
async fn resource_put_conflict_returns_409() {
    let state = test_state().await;
    let app = app_router(state.clone());

    let first_req = Request::builder()
        .method("PUT")
        .uri("/resource/mission/demo/card-1")
        .header("content-type", "application/json")
        .body(Body::from(
            json!({
                "value": {"title":"Card 1"},
                "updated_by": "agent-1"
            })
            .to_string(),
        ))
        .expect("first request");
    let first_resp = app
        .clone()
        .oneshot(first_req)
        .await
        .expect("first response");
    assert_eq!(first_resp.status(), StatusCode::OK);

    let conflict_req = Request::builder()
        .method("PUT")
        .uri("/resource/mission/demo/card-1")
        .header("content-type", "application/json")
        .body(Body::from(
            json!({
                "value": {"title":"Card 1 updated"},
                "if_match_rev": 99,
                "updated_by": "agent-2"
            })
            .to_string(),
        ))
        .expect("conflict request");
    let conflict_resp = app
        .clone()
        .oneshot(conflict_req)
        .await
        .expect("conflict response");
    assert_eq!(conflict_resp.status(), StatusCode::CONFLICT);
}

#[tokio::test]
async fn resource_updated_event_contract_snapshot() {
    let state = test_state().await;
    let mut rx = state.event_bus.subscribe();
    let app = app_router(state.clone());

    let put_req = Request::builder()
        .method("PUT")
        .uri("/resource/project/demo/board")
        .header("content-type", "application/json")
        .body(Body::from(
            json!({
                "value": {"status":"todo"},
                "updated_by": "agent-1"
            })
            .to_string(),
        ))
        .expect("put request");
    let put_resp = app.clone().oneshot(put_req).await.expect("put response");
    assert_eq!(put_resp.status(), StatusCode::OK);

    let event = tokio::time::timeout(Duration::from_secs(5), async {
        loop {
            let event = rx.recv().await.expect("event");
            if event.event_type == "resource.updated" {
                return event;
            }
        }
    })
    .await
    .expect("resource.updated timeout");

    let mut properties = event
        .properties
        .as_object()
        .cloned()
        .expect("resource.updated properties object");
    let updated_at_ms = properties
        .remove("updatedAtMs")
        .and_then(|v| v.as_u64())
        .expect("updatedAtMs");
    assert!(updated_at_ms > 0);

    let snapshot = json!({
        "type": event.event_type,
        "properties": properties,
    });
    let expected = json!({
        "type": "resource.updated",
        "properties": {
            "key": "project/demo/board",
            "rev": 1,
            "updatedBy": "agent-1"
        }
    });
    assert_eq!(snapshot, expected);
}

#[tokio::test]
async fn resource_deleted_event_contract_snapshot() {
    let state = test_state().await;
    let mut rx = state.event_bus.subscribe();
    let app = app_router(state.clone());

    let put_req = Request::builder()
        .method("PUT")
        .uri("/resource/project/demo/board")
        .header("content-type", "application/json")
        .body(Body::from(
            json!({
                "value": {"status":"todo"},
                "updated_by": "agent-1"
            })
            .to_string(),
        ))
        .expect("put request");
    let put_resp = app.clone().oneshot(put_req).await.expect("put response");
    assert_eq!(put_resp.status(), StatusCode::OK);

    let delete_req = Request::builder()
        .method("DELETE")
        .uri("/resource/project/demo/board")
        .header("content-type", "application/json")
        .body(Body::from(
            json!({
                "if_match_rev": 1,
                "updated_by": "reviewer-1"
            })
            .to_string(),
        ))
        .expect("delete request");
    let delete_resp = app
        .clone()
        .oneshot(delete_req)
        .await
        .expect("delete response");
    assert_eq!(delete_resp.status(), StatusCode::OK);

    let event = tokio::time::timeout(Duration::from_secs(5), async {
        loop {
            let event = rx.recv().await.expect("event");
            if event.event_type == "resource.deleted" {
                return event;
            }
        }
    })
    .await
    .expect("resource.deleted timeout");

    let mut properties = event
        .properties
        .as_object()
        .cloned()
        .expect("resource.deleted properties object");
    let updated_at_ms = properties
        .remove("updatedAtMs")
        .and_then(|v| v.as_u64())
        .expect("updatedAtMs");
    assert!(updated_at_ms > 0);

    let snapshot = json!({
        "type": event.event_type,
        "properties": properties,
    });
    let expected = json!({
        "type": "resource.deleted",
        "properties": {
            "key": "project/demo/board",
            "rev": 1,
            "updatedBy": "reviewer-1"
        }
    });
    assert_eq!(snapshot, expected);
}