Add Outline API client library foundation
Implements a first vertical slice of the Outline RPC API (auth, documents, collections, users) covering the structural patterns used across the whole API: single object, paginated list, tree, create/update, delete, search. Chosen as the basis for future CLI and GUI clients built on top of this crate. - Handwritten client (not codegen) against the vendored OpenAPI spec, since Outline's API is uniformly POST /api/<resource>.<action> with JSON bodies and inline/anonymous schemas that generators handle poorly - Async (reqwest + tokio) Client, cheaply cloneable, no &mut self methods - thiserror-based Error with ErrorKind classification and boxed API error context; forward-compatible models (unknown fields ignored, unknown string-enum values preserved via a catch-all variant) since the API is unversioned and self-hosted instances vary in age - Pagination via Page<T>/Paginator with next_page/collect_all/into_stream - wiremock-based test suite plus a spec-coverage test guarding against typos in RPC method names
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
mod common;
|
||||
|
||||
use serde_json::json;
|
||||
use wiremock::matchers::{body_json, method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
#[tokio::test]
|
||||
async fn collections_info_returns_collection() {
|
||||
let server = MockServer::start().await;
|
||||
let client = common::client_for(&server).await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/collections.info"))
|
||||
.and(body_json(json!({ "id": "col-1" })))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"ok": true,
|
||||
"data": { "id": "col-1", "name": "Human Resources", "sharing": false }
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let collection = client.collections().info("col-1").await.unwrap();
|
||||
assert_eq!(collection.name, "Human Resources");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn collections_list_returns_page_with_policies() {
|
||||
let server = MockServer::start().await;
|
||||
let client = common::client_for(&server).await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/collections.list"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"ok": true,
|
||||
"data": [{ "id": "col-1", "name": "Human Resources", "sharing": false }],
|
||||
"pagination": { "limit": 25, "offset": 0 },
|
||||
"policies": [{ "id": "col-1", "abilities": { "update": true, "delete": false } }]
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let page = client.collections().list().send().await.unwrap();
|
||||
assert_eq!(page.items.len(), 1);
|
||||
assert_eq!(page.policies.len(), 1);
|
||||
assert!(page.policies[0].can("update"));
|
||||
assert!(!page.policies[0].can("delete"));
|
||||
assert!(!page.policies[0].can("archive"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn collections_documents_returns_nested_tree() {
|
||||
let server = MockServer::start().await;
|
||||
let client = common::client_for(&server).await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/collections.documents"))
|
||||
.and(body_json(json!({ "id": "col-1" })))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"ok": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "doc-1",
|
||||
"title": "Parent",
|
||||
"children": [
|
||||
{ "id": "doc-2", "title": "Child", "children": [] }
|
||||
]
|
||||
}
|
||||
]
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let tree = client.collections().documents("col-1").await.unwrap();
|
||||
assert_eq!(tree.len(), 1);
|
||||
assert_eq!(tree[0].title, "Parent");
|
||||
assert_eq!(tree[0].children.len(), 1);
|
||||
assert_eq!(tree[0].children[0].title, "Child");
|
||||
}
|
||||
Reference in New Issue
Block a user