use crate::cli::{print_value, DaemonClient};
use anyhow::{ensure, Context, Result};
use serde_json::{json, Value};
pub async fn list(client: &DaemonClient) -> Result<()> {
client.ensure_running().await?;
let resp = client.get("/groups").await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn create(
client: &DaemonClient,
name: &str,
description: Option<&str>,
display_name: Option<&str>,
preset: Option<&str>,
) -> Result<()> {
client.ensure_running().await?;
let mut body = json!({ "name": name });
if let Some(desc) = description {
body["description"] = Value::String(desc.to_string());
}
if let Some(dn) = display_name {
body["display_name"] = Value::String(dn.to_string());
}
if let Some(p) = preset {
body["preset"] = Value::String(p.to_string());
}
let resp = client.post("/groups", &body).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn info(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client.get(&format!("/groups/{group_id}")).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn update(
client: &DaemonClient,
group_id: &str,
name: Option<&str>,
description: Option<&str>,
) -> Result<()> {
ensure!(
name.is_some() || description.is_some(),
"group update requires at least one of: --name, --description"
);
client.ensure_running().await?;
let mut body = json!({});
if let Some(n) = name {
body["name"] = Value::String(n.to_string());
}
if let Some(d) = description {
body["description"] = Value::String(d.to_string());
}
let resp = client.patch(&format!("/groups/{group_id}"), &body).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn members(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client.get(&format!("/groups/{group_id}/members")).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn add_member(
client: &DaemonClient,
group_id: &str,
agent_id: &str,
display_name: Option<&str>,
) -> Result<()> {
client.ensure_running().await?;
let mut body = json!({ "agent_id": agent_id });
if let Some(dn) = display_name {
body["display_name"] = Value::String(dn.to_string());
}
let resp = client
.post(&format!("/groups/{group_id}/members"), &body)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn remove_member(client: &DaemonClient, group_id: &str, agent_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client
.delete(&format!("/groups/{group_id}/members/{agent_id}"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn invite(client: &DaemonClient, group_id: &str, expiry_secs: u64) -> Result<()> {
client.ensure_running().await?;
let body = json!({ "expiry_secs": expiry_secs });
let resp = client
.post(&format!("/groups/{group_id}/invite"), &body)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn join(
client: &DaemonClient,
invite_link: &str,
display_name: Option<&str>,
) -> Result<()> {
client.ensure_running().await?;
let mut body = json!({ "invite": invite_link });
if let Some(dn) = display_name {
body["display_name"] = Value::String(dn.to_string());
}
let resp = client.post("/groups/join", &body).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn set_name(client: &DaemonClient, group_id: &str, name: &str) -> Result<()> {
client.ensure_running().await?;
let body = json!({ "name": name });
let resp = client
.put(&format!("/groups/{group_id}/display-name"), &body)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn leave(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client.delete(&format!("/groups/{group_id}")).await?;
print_value(client.format(), &resp);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn policy(
client: &DaemonClient,
group_id: &str,
preset: Option<&str>,
discoverability: Option<&str>,
admission: Option<&str>,
confidentiality: Option<&str>,
read_access: Option<&str>,
write_access: Option<&str>,
) -> Result<()> {
ensure!(
preset.is_some()
|| discoverability.is_some()
|| admission.is_some()
|| confidentiality.is_some()
|| read_access.is_some()
|| write_access.is_some(),
"group policy requires at least one of: --preset, --discoverability, --admission, --confidentiality, --read-access, --write-access"
);
client.ensure_running().await?;
let mut body = json!({});
if let Some(v) = preset {
body["preset"] = Value::String(v.to_string());
}
if let Some(v) = discoverability {
body["discoverability"] = Value::String(v.to_string());
}
if let Some(v) = admission {
body["admission"] = Value::String(v.to_string());
}
if let Some(v) = confidentiality {
body["confidentiality"] = Value::String(v.to_string());
}
if let Some(v) = read_access {
body["read_access"] = Value::String(v.to_string());
}
if let Some(v) = write_access {
body["write_access"] = Value::String(v.to_string());
}
let resp = client
.patch(&format!("/groups/{group_id}/policy"), &body)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn set_role(
client: &DaemonClient,
group_id: &str,
agent_id: &str,
role: &str,
) -> Result<()> {
client.ensure_running().await?;
let body = json!({ "role": role });
let resp = client
.patch(
&format!("/groups/{group_id}/members/{agent_id}/role"),
&body,
)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn ban(client: &DaemonClient, group_id: &str, agent_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client
.post_empty(&format!("/groups/{group_id}/ban/{agent_id}"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn unban(client: &DaemonClient, group_id: &str, agent_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client
.delete(&format!("/groups/{group_id}/ban/{agent_id}"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn requests(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client.get(&format!("/groups/{group_id}/requests")).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn request_access(
client: &DaemonClient,
group_id: &str,
message: Option<&str>,
) -> Result<()> {
client.ensure_running().await?;
let body = match message {
Some(m) => json!({ "message": m }),
None => json!({}),
};
let resp = client
.post(&format!("/groups/{group_id}/requests"), &body)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn approve_request(
client: &DaemonClient,
group_id: &str,
request_id: &str,
) -> Result<()> {
client.ensure_running().await?;
let resp = client
.post_empty(&format!("/groups/{group_id}/requests/{request_id}/approve"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn reject_request(client: &DaemonClient, group_id: &str, request_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client
.post_empty(&format!("/groups/{group_id}/requests/{request_id}/reject"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn cancel_request(client: &DaemonClient, group_id: &str, request_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client
.delete(&format!("/groups/{group_id}/requests/{request_id}"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn discover(client: &DaemonClient, query: Option<&str>) -> Result<()> {
client.ensure_running().await?;
let resp = match query {
Some(q) if !q.is_empty() => client.get_query("/groups/discover", &[("q", q)]).await?,
_ => client.get("/groups/discover").await?,
};
print_value(client.format(), &resp);
Ok(())
}
pub async fn discover_nearby(client: &DaemonClient) -> Result<()> {
client.ensure_running().await?;
let resp = client.get("/groups/discover/nearby").await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn discover_subscriptions(client: &DaemonClient) -> Result<()> {
client.ensure_running().await?;
let resp = client.get("/groups/discover/subscriptions").await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn discover_subscribe(
client: &DaemonClient,
kind: &str,
key: Option<&str>,
shard: Option<u32>,
) -> Result<()> {
client.ensure_running().await?;
let mut body = json!({ "kind": kind });
if let Some(k) = key {
body["key"] = Value::String(k.to_string());
}
if let Some(s) = shard {
body["shard"] = Value::Number(s.into());
}
let resp = client.post("/groups/discover/subscribe", &body).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn discover_unsubscribe(client: &DaemonClient, kind: &str, shard: u32) -> Result<()> {
client.ensure_running().await?;
let resp = client
.delete(&format!("/groups/discover/subscribe/{kind}/{shard}"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn card(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client.get(&format!("/groups/cards/{group_id}")).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn card_import(client: &DaemonClient, path: &str) -> Result<()> {
client.ensure_running().await?;
let raw = if path == "-" {
use std::io::Read as _;
let mut s = String::new();
std::io::stdin()
.read_to_string(&mut s)
.context("read card from stdin")?;
s
} else {
std::fs::read_to_string(path).with_context(|| format!("read card from {path}"))?
};
let card: Value = serde_json::from_str(&raw).context("parse card JSON")?;
let resp = client.post("/groups/cards/import", &card).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn send(
client: &DaemonClient,
group_id: &str,
body_text: &str,
kind: Option<&str>,
) -> Result<()> {
client.ensure_running().await?;
let mut req = json!({ "body": body_text });
if let Some(k) = kind {
req["kind"] = Value::String(k.to_string());
}
let resp = client
.post(&format!("/groups/{group_id}/send"), &req)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn messages(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client.get(&format!("/groups/{group_id}/messages")).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn state(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client.get(&format!("/groups/{group_id}/state")).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn state_seal(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client
.post_empty(&format!("/groups/{group_id}/state/seal"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn state_withdraw(client: &DaemonClient, group_id: &str) -> Result<()> {
client.ensure_running().await?;
let resp = client
.post_empty(&format!("/groups/{group_id}/state/withdraw"))
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn secure_encrypt(client: &DaemonClient, group_id: &str, payload: &[u8]) -> Result<()> {
use base64::Engine as _;
client.ensure_running().await?;
let payload_b64 = base64::engine::general_purpose::STANDARD.encode(payload);
let body = json!({ "payload_b64": payload_b64 });
let resp = client
.post(&format!("/groups/{group_id}/secure/encrypt"), &body)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn secure_decrypt(
client: &DaemonClient,
group_id: &str,
ciphertext_b64: &str,
nonce_b64: &str,
secret_epoch: u64,
) -> Result<()> {
client.ensure_running().await?;
let body = json!({
"ciphertext_b64": ciphertext_b64,
"nonce_b64": nonce_b64,
"secret_epoch": secret_epoch,
});
let resp = client
.post(&format!("/groups/{group_id}/secure/decrypt"), &body)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn secure_reseal(client: &DaemonClient, group_id: &str, recipient: &str) -> Result<()> {
client.ensure_running().await?;
let body = json!({ "recipient": recipient });
let resp = client
.post(&format!("/groups/{group_id}/secure/reseal"), &body)
.await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn secure_open_envelope(client: &DaemonClient, path: &str) -> Result<()> {
client.ensure_running().await?;
let raw = if path == "-" {
use std::io::Read as _;
let mut s = String::new();
std::io::stdin()
.read_to_string(&mut s)
.context("read envelope from stdin")?;
s
} else {
std::fs::read_to_string(path).with_context(|| format!("read envelope from {path}"))?
};
let envelope: Value = serde_json::from_str(&raw).context("parse envelope JSON")?;
let resp = client
.post("/groups/secure/open-envelope", &envelope)
.await?;
print_value(client.format(), &resp);
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::cli::DaemonClient;
#[allow(dead_code)]
async fn start_mock_server(
response_json: serde_json::Value,
) -> (String, tokio::sync::oneshot::Sender<()>) {
use std::sync::Arc;
let json = Arc::new(response_json);
let app = axum::Router::new().fallback(move |_req: axum::extract::Request| {
let json = Arc::clone(&json);
async move {
let body = serde_json::to_vec(&*json).unwrap();
axum::response::Response::builder()
.status(200)
.header("content-type", "application/json")
.body(axum::body::Body::from(body))
.unwrap()
}
});
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
axum::serve(listener, app.into_make_service())
.with_graceful_shutdown(async {
rx.await.ok();
})
.await
.ok();
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
(format!("http://{}", addr), tx)
}
#[tokio::test]
async fn list_returns_mock_response() {
let mock_resp = serde_json::json!({"status": "ok"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = list(&client).await;
assert!(result.is_ok(), "list should succeed: {:?}", result);
}
#[tokio::test]
async fn info_returns_mock_response() {
let mock_resp = serde_json::json!({"status": "ok"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = info(&client, "group-123").await;
assert!(result.is_ok(), "info should succeed: {:?}", result);
}
#[tokio::test]
async fn members_returns_mock_response() {
let mock_resp = serde_json::json!({"status": "ok"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = members(&client, "group-123").await;
assert!(result.is_ok(), "members should succeed: {:?}", result);
}
#[tokio::test]
async fn leave_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = leave(&client, "group-123").await;
assert!(result.is_ok(), "leave should succeed: {:?}", result);
}
#[tokio::test]
async fn requests_returns_mock_response() {
let mock_resp = serde_json::json!([{"request_id": "req-1"}]);
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = requests(&client, "group-123").await;
assert!(result.is_ok(), "requests should succeed: {:?}", result);
}
#[tokio::test]
async fn cancel_request_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = cancel_request(&client, "group-123", "req-1").await;
assert!(
result.is_ok(),
"cancel_request should succeed: {:?}",
result
);
}
#[tokio::test]
async fn discover_returns_mock_response() {
let mock_resp = serde_json::json!([{"id": "group-1"}]);
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = discover(&client, Some("test")).await;
assert!(result.is_ok(), "discover should succeed: {:?}", result);
}
#[tokio::test]
async fn discover_nearby_returns_mock_response() {
let mock_resp = serde_json::json!([{"id": "group-1"}]);
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = discover_nearby(&client).await;
assert!(
result.is_ok(),
"discover_nearby should succeed: {:?}",
result
);
}
#[tokio::test]
async fn discover_subscriptions_returns_mock_response() {
let mock_resp = serde_json::json!([{"kind": "tag", "shard": 42}]);
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = discover_subscriptions(&client).await;
assert!(
result.is_ok(),
"discover_subscriptions should succeed: {:?}",
result
);
}
#[tokio::test]
async fn card_returns_mock_response() {
let mock_resp = serde_json::json!({"id": "group-1", "name": "test"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = card(&client, "group-123").await;
assert!(result.is_ok(), "card should succeed: {:?}", result);
}
#[tokio::test]
async fn messages_returns_mock_response() {
let mock_resp = serde_json::json!([{"id": "msg-1", "text": "hello"}]);
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = messages(&client, "group-123").await;
assert!(result.is_ok(), "messages should succeed: {:?}", result);
}
#[tokio::test]
async fn state_returns_mock_response() {
let mock_resp = serde_json::json!({"version": 1, "sealed": false});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = state(&client, "group-123").await;
assert!(result.is_ok(), "state should succeed: {:?}", result);
}
#[tokio::test]
async fn state_seal_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = state_seal(&client, "group-123").await;
assert!(result.is_ok(), "state_seal should succeed: {:?}", result);
}
#[tokio::test]
async fn state_withdraw_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = state_withdraw(&client, "group-123").await;
assert!(
result.is_ok(),
"state_withdraw should succeed: {:?}",
result
);
}
#[tokio::test]
async fn set_name_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = set_name(&client, "group-123", "new-name").await;
assert!(result.is_ok(), "set_name should succeed: {:?}", result);
}
#[tokio::test]
async fn ban_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = ban(&client, "group-123", "agent-456").await;
assert!(result.is_ok(), "ban should succeed: {:?}", result);
}
#[tokio::test]
async fn unban_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = unban(&client, "group-123", "agent-456").await;
assert!(result.is_ok(), "unban should succeed: {:?}", result);
}
#[tokio::test]
async fn reject_request_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = reject_request(&client, "group-123", "req-1").await;
assert!(
result.is_ok(),
"reject_request should succeed: {:?}",
result
);
}
#[tokio::test]
async fn invite_returns_mock_response() {
let mock_resp = serde_json::json!({"invite_code": "abc123"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = invite(&client, "group-123", 3600).await;
assert!(result.is_ok(), "invite should succeed: {:?}", result);
}
#[tokio::test]
async fn create_returns_mock_response() {
let mock_resp = serde_json::json!({"id": "new-group", "name": "test"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = create(&client, "test-group", None, None, None).await;
assert!(result.is_ok(), "create should succeed: {:?}", result);
}
#[tokio::test]
async fn add_member_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = add_member(&client, "group-123", "agent-456", None).await;
assert!(result.is_ok(), "add_member should succeed: {:?}", result);
}
#[tokio::test]
async fn remove_member_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = remove_member(&client, "group-123", "agent-456").await;
assert!(result.is_ok(), "remove_member should succeed: {:?}", result);
}
#[tokio::test]
async fn join_returns_mock_response() {
let mock_resp = serde_json::json!({"id": "joined-group"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = join(&client, "invite-code-123", None).await;
assert!(result.is_ok(), "join should succeed: {:?}", result);
}
#[tokio::test]
async fn request_access_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = request_access(&client, "group-123", None).await;
assert!(
result.is_ok(),
"request_access should succeed: {:?}",
result
);
}
#[tokio::test]
async fn approve_request_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = approve_request(&client, "group-123", "req-1").await;
assert!(
result.is_ok(),
"approve_request should succeed: {:?}",
result
);
}
#[tokio::test]
async fn discover_subscribe_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = discover_subscribe(&client, "tag", None, None).await;
assert!(
result.is_ok(),
"discover_subscribe should succeed: {:?}",
result
);
}
#[tokio::test]
async fn discover_unsubscribe_returns_mock_response() {
let mock_resp = serde_json::json!({"ok": true});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = discover_unsubscribe(&client, "tag", 42).await;
assert!(
result.is_ok(),
"discover_unsubscribe should succeed: {:?}",
result
);
}
#[tokio::test]
async fn secure_encrypt_returns_mock_response() {
let mock_resp = serde_json::json!({"ciphertext": "encrypted-data"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = secure_encrypt(&client, "group-123", b"hello").await;
assert!(
result.is_ok(),
"secure_encrypt should succeed: {:?}",
result
);
}
#[tokio::test]
async fn secure_reseal_returns_mock_response() {
let mock_resp = serde_json::json!({"ciphertext": "resealed-data"});
let (url, _shutdown) = start_mock_server(mock_resp).await;
let client = DaemonClient::new(None, Some(&url), crate::cli::OutputFormat::Json).unwrap();
let result = secure_reseal(&client, "group-123", "agent-456").await;
assert!(result.is_ok(), "secure_reseal should succeed: {:?}", result);
}
}