context69-sdk 0.7.0

Async Rust SDK for the Context69 HTTP API.
Documentation
use axum::http::{Method, StatusCode, header};
use context69_contracts::{
    ImportLibraryFileFromUrlRequest, LibraryFileUploadMetadata, LibraryTextContentFormat,
    MoveFolderRequest, UpsertLibraryTextRequest,
};
use reqwest::multipart::Part;
use serde_json::json;
use uuid::Uuid;

use super::support::{client, spawn_empty, spawn_json};

fn tree_json() -> serde_json::Value {
    json!({
        "root": {
            "group_key": "", "group_path": "", "visibility": "private",
            "folder_id": null, "parent_folder_id": null, "name": "Library",
            "path": "/", "processing_count": 0, "children": [], "files": []
        }
    })
}

fn file_json(file_id: Uuid) -> serde_json::Value {
    json!({
        "file_id": file_id, "group_key": "ops", "group_path": "ops/platform",
        "visibility": "private", "folder_id": null, "folder_path": "/",
        "filename": "runbook.md", "media_type": "text/markdown", "size_bytes": 8,
        "sha256": "abc", "ingest_status": "succeeded", "error_message": null,
        "created_at": "2026-07-10T00:00:00Z", "updated_at": "2026-07-10T00:00:00Z",
        "ingested_at": "2026-07-10T00:00:00Z", "sections": [], "jobs": []
    })
}

fn url_import_job_json(job_id: Uuid) -> serde_json::Value {
    json!({
        "import_job_id": job_id, "group_path": "ops/platform",
        "source_url": "https://files.example.test/report.pdf", "status": "queued",
        "attempt_count": 0, "created_at": "2026-07-12T00:00:00Z",
        "started_at": null, "finished_at": null, "updated_at": "2026-07-12T00:00:00Z"
    })
}

#[tokio::test]
async fn group_library_imports_public_url() {
    let job_id = Uuid::new_v4();
    let (base_url, captured) = spawn_json(StatusCode::ACCEPTED, &url_import_job_json(job_id)).await;
    let request = ImportLibraryFileFromUrlRequest {
        url: "https://files.example.test/report.pdf".into(),
        folder_id: None,
        filename: None,
        media_type: None,
        metadata: Some(LibraryFileUploadMetadata {
            external_id: Some("report-42".into()),
            ..Default::default()
        }),
        translation: None,
    };
    let job = client(&base_url)
        .group("ops/platform")
        .library()
        .files()
        .import_url(&request)
        .await
        .expect("import URL");
    let captured = captured.await.expect("captured request");
    assert_eq!(job.import_job_id, job_id);
    assert_eq!(captured.method, Method::POST);
    assert_eq!(
        captured.uri.path(),
        "/v1/groups/by-path/ops%2Fplatform/library/files/import-url"
    );
    assert_eq!(
        serde_json::from_slice::<serde_json::Value>(&captured.body).unwrap()["metadata"]["external_id"],
        "report-42"
    );
}

#[tokio::test]
async fn group_library_url_import_job_get_uses_group_scope() {
    let job_id = Uuid::new_v4();
    let (base_url, captured) = spawn_json(StatusCode::OK, &url_import_job_json(job_id)).await;
    client(&base_url)
        .group("ops/platform")
        .library()
        .url_import_job(job_id)
        .get()
        .await
        .expect("get URL import job");
    let captured = captured.await.expect("captured request");
    assert_eq!(
        captured.uri.path(),
        format!("/v1/groups/by-path/ops%2Fplatform/library/url-import-jobs/{job_id}")
    );
}

#[tokio::test]
async fn global_library_tree_uses_global_scope() {
    let (base_url, captured) = spawn_json(StatusCode::OK, &tree_json()).await;
    let tree = client(&base_url).library().tree().await.expect("tree");
    let request = captured.await.expect("captured request");
    assert_eq!(tree.root.path, "/");
    assert_eq!(request.uri.path(), "/v1/library/tree");
}

#[tokio::test]
async fn group_library_text_upsert_uses_group_scope() {
    let response = json!({"files": [], "jobs": []});
    let (base_url, captured) = spawn_json(StatusCode::OK, &response).await;
    let input = UpsertLibraryTextRequest {
        external_id: "incident-42".to_string(),
        folder_id: None,
        title: "Incident".to_string(),
        content: "body".to_string(),
        content_format: LibraryTextContentFormat::Markdown,
        source_uri: None,
        summary: None,
        published_at: None,
        metadata_json: json!({}),
        translation: None,
    };
    client(&base_url)
        .group("ops/platform")
        .library()
        .texts()
        .upsert(&input)
        .await
        .expect("upsert text");
    let request = captured.await.expect("captured request");
    assert_eq!(request.method, Method::PUT);
    assert_eq!(
        request.uri.path(),
        "/v1/groups/by-path/ops%2Fplatform/library/texts"
    );
}

#[tokio::test]
async fn temporary_group_library_file_chain_gets_file() {
    let file_id = Uuid::new_v4();
    let (base_url, captured) = spawn_json(StatusCode::OK, &file_json(file_id)).await;
    let file = client(&base_url)
        .group("ops/platform")
        .library()
        .file(file_id)
        .get()
        .await
        .expect("get file");
    let request = captured.await.expect("captured request");
    assert_eq!(file.file_id, file_id);
    assert_eq!(
        request.uri.path(),
        format!("/v1/groups/by-path/ops%2Fplatform/library/files/{file_id}")
    );
}

#[tokio::test]
async fn library_folder_resource_moves() {
    let folder_id = Uuid::new_v4();
    let response = json!({
        "folder_id": folder_id, "group_key": "", "group_path": "",
        "visibility": "private", "parent_folder_id": null, "name": "Docs", "path": "/Docs",
        "created_at": "2026-07-10T00:00:00Z", "updated_at": "2026-07-10T00:00:00Z"
    });
    let (base_url, captured) = spawn_json(StatusCode::OK, &response).await;
    client(&base_url)
        .library()
        .folder(folder_id)
        .move_to(&MoveFolderRequest {
            target_folder_id: None,
        })
        .await
        .expect("move folder");
    let request = captured.await.expect("captured request");
    assert_eq!(request.method, Method::POST);
    assert_eq!(
        request.uri.path(),
        format!("/v1/library/folders/{folder_id}/move")
    );
}

#[tokio::test]
async fn library_files_upload_builds_multipart_form() {
    let folder_id = Uuid::new_v4();
    let response = json!({"files": [], "jobs": []});
    let (base_url, captured) = spawn_json(StatusCode::OK, &response).await;
    client(&base_url)
        .library()
        .files()
        .upload(
            Some(folder_id),
            vec![Part::text("hello").file_name("note.md")],
        )
        .await
        .expect("upload files");
    let request = captured.await.expect("captured request");
    assert_eq!(request.uri.path(), "/v1/library/files/upload");
    assert!(
        request.headers[header::CONTENT_TYPE]
            .to_str()
            .unwrap()
            .starts_with("multipart/form-data; boundary=")
    );
    let body = String::from_utf8_lossy(&request.body);
    assert!(body.contains(&folder_id.to_string()));
    assert!(body.contains("filename=\"note.md\""));
}

#[tokio::test]
async fn deduplicated_upload_sends_metadata_and_reuses_existing_file() {
    let file_id = Uuid::new_v4();
    let response = json!({
        "upload_required": false,
        "file": file_json(file_id),
        "job": null
    });
    let (base_url, captured) = spawn_json(StatusCode::OK, &response).await;
    let metadata = LibraryFileUploadMetadata {
        external_id: Some("report-42".to_string()),
        source_uri: Some("https://example.test/report.pdf".to_string()),
        published_at: Some("2026-07-12T09:30:00+08:00".parse().unwrap()),
        metadata_json: json!({"ticker": "ACME"}),
    };

    let upload = client(&base_url)
        .group("ops/platform")
        .library()
        .files()
        .upload_bytes_deduplicated_with_metadata(
            None,
            "report.pdf",
            "application/pdf",
            b"pdf".to_vec(),
            Some(metadata),
        )
        .await
        .expect("reuse upload");

    assert_eq!(upload.files[0].file_id, file_id);
    assert!(upload.jobs.is_empty());
    let request = captured.await.expect("captured request");
    assert_eq!(
        request.uri.path(),
        "/v1/groups/by-path/ops%2Fplatform/library/files/prepare-upload"
    );
    let body: serde_json::Value = serde_json::from_slice(&request.body).unwrap();
    assert_eq!(body["metadata"]["external_id"], "report-42");
    assert_eq!(body["metadata"]["metadata_json"]["ticker"], "ACME");
    assert_eq!(body["metadata"]["published_at"], "2026-07-12T01:30:00Z");
}

#[tokio::test]
async fn library_file_delete_uses_item_endpoint() {
    let file_id = Uuid::new_v4();
    let (base_url, captured) = spawn_empty(StatusCode::NO_CONTENT).await;
    client(&base_url)
        .library()
        .file(file_id)
        .delete()
        .await
        .expect("delete file");
    assert_eq!(captured.await.unwrap().method, Method::DELETE);
}