wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
use super::*;

use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;

fn git(dir: &std::path::Path, args: &[&str]) {
    let output = wsb::git_backend::git_command()
        .arg("-C")
        .arg(dir)
        .args(args)
        .output()
        .expect("git runs");
    assert!(output.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&output.stderr));
}

async fn call(
    router: &Router,
    method: &str,
    uri: &str,
    body: Option<serde_json::Value>,
) -> (StatusCode, serde_json::Value) {
    let request = Request::builder()
        .method(method)
        .uri(uri)
        .header("content-type", "application/json")
        .body(body.map_or_else(Body::empty, |body| Body::from(body.to_string())))
        .expect("a request");
    let response = router.clone().oneshot(request).await.expect("the router answers");
    let status = response.status();
    let bytes = hyper::body::to_bytes(response.into_body()).await.expect("a body");
    (status, serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null))
}

/// TEST20122: a page writes notes and links into the worktree it names and
/// reads them back by what they are about: a note about a file is found by
/// that file, a link is made and removed, removing the note takes its links,
/// and a worktree that is not there, a ref that does not parse and a field
/// the route does not take are each refused as such.
#[tokio::test]
async fn test20122_notes_and_links_are_written_where_the_page_says_and_read_by_ref() {
    let prj = tempfile::tempdir().expect("a workshop");
    let machine_dir = tempfile::tempdir().expect("a machine folder");
    let root = wsb::platform::canonicalize(prj.path()).expect("resolves");
    let _env = wsb::test_environment::Environment::set(&[
        ("PRJ", &root.to_string_lossy()),
        (wsb::daemon::MACHINE_DIR_ENV, &machine_dir.path().to_string_lossy()),
    ]);
    let main = root.join("names/main-worktree");
    std::fs::create_dir_all(main.join("src")).expect("a worktree");
    git(&main, &["init", "--quiet"]);
    git(&main, &["remote", "add", "origin", "git@github.com:owner/names.git"]);
    wsb::settings::RepoSettings {
        name: "names".to_string(),
        alias: "names".to_string(),
        templates_root: ".".to_string(),
        version: wsb::settings::VersionSettings::at_major(0),
    }
    .save(&main)
    .expect("managed");
    let machine = crate::machine::MachineState::new(
        machine_dir.path().to_path_buf(),
        &machine_dir.path().join("wsb.db"),
        std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
        0,
        std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
        0,
    )
    .expect("a machine");
    machine.identity.rename(machine_dir.path(), "bench").expect("named");
    let router = routes().with_state(machine);
    let file = "path:owner/names/src/lib.rs";

    let (status, made) = call(
        &router,
        "POST",
        "/api/v1/annotations/notes",
        Some(json!({ "worktree": "names:main", "title": "Lazy parser", "body": "Because.", "about": [file] })),
    )
    .await;
    assert_eq!(status, StatusCode::CREATED, "{made}");
    let id = made["note"]["id"].as_str().expect("an id").to_string();
    assert_eq!(made["note"]["machine"], "bench", "the note says which machine wrote it");
    assert!(main.join(format!(".wsb/notes/{id}.md")).is_file(), "the note is in the worktree it names");

    let (status, about) = call(&router, "GET", &format!("/api/v1/annotations?ref={file}"), None).await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(about["notes"].as_array().map(Vec::len), Some(1), "{about}");
    assert_eq!(about["notes"][0]["id"], id.as_str());
    let (_, unrelated) = call(&router, "GET", "/api/v1/annotations?ref=path:owner/names/README.md", None).await;
    assert_eq!(unrelated["notes"].as_array().map(Vec::len), Some(0), "{unrelated}");

    let link = json!({ "worktree": "names:main", "from": "project:owner/names", "to": "url:https://example.com/spec", "label": "spec" });
    let (status, linked) = call(&router, "POST", "/api/v1/annotations/links", Some(link.clone())).await;
    assert_eq!(status, StatusCode::CREATED, "{linked}");
    let (_, at) = call(&router, "GET", "/api/v1/annotations?ref=project:owner/names", None).await;
    assert_eq!(at["links"].as_array().map(Vec::len), Some(1), "{at}");
    let (status, _) = call(&router, "DELETE", "/api/v1/annotations/links", Some(link)).await;
    assert_eq!(status, StatusCode::OK);

    let (status, removed) =
        call(&router, "DELETE", &format!("/api/v1/annotations/notes/{id}?worktree=names:main"), None).await;
    assert_eq!(status, StatusCode::OK, "{removed}");
    assert_eq!(removed["removed"].as_array().map(Vec::len), Some(2), "the note and its link: {removed}");
    let (_, everything) = call(&router, "GET", "/api/v1/annotations", None).await;
    assert_eq!(everything["notes"].as_array().map(Vec::len), Some(0), "{everything}");
    assert_eq!(everything["links"].as_array().map(Vec::len), Some(0), "{everything}");

    let note = |worktree: &str, about: &str| {
        Some(json!({ "worktree": worktree, "title": "t", "about": [about] }))
    };
    let (status, _) = call(&router, "POST", "/api/v1/annotations/notes", note("names:fix", file)).await;
    assert_eq!(status, StatusCode::NOT_FOUND, "a worktree that is not there");
    let (status, _) = call(&router, "POST", "/api/v1/annotations/notes", note("names:main", "nonsense")).await;
    assert_eq!(status, StatusCode::BAD_REQUEST, "a ref that does not parse");
    let (status, _) = call(
        &router,
        "POST",
        "/api/v1/annotations/notes",
        Some(json!({ "worktree": "names:main", "title": "t", "pinned": true })),
    )
    .await;
    assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "a field the route does not take");
    assert!(!main.join(".wsb/notes").read_dir().map(|mut d| d.next().is_some()).unwrap_or(false), "a refused note was written");
}