use magnetar_admin::{AdminClient, AdminError};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn client(mock: &MockServer) -> AdminClient {
AdminClient::builder()
.service_url(mock.uri().parse().unwrap())
.build()
.unwrap()
}
#[tokio::test]
async fn decode_error_on_empty_body_names_method_url_status() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/clusters"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let err = admin.cluster_list().await.unwrap_err();
match &err {
AdminError::Decode {
method,
url,
status,
content_type: _,
snippet: _,
..
} => {
assert_eq!(method, "GET");
assert!(url.contains("/admin/v2/clusters"), "url was {url}");
assert_eq!(*status, 200);
}
other => panic!("expected AdminError::Decode, got {other:?}"),
}
let display = err.to_string();
assert!(display.contains("GET"), "display: {display}");
assert!(display.contains("/admin/v2/clusters"), "display: {display}");
assert!(display.contains("HTTP 200"), "display: {display}");
assert!(
!display.contains("expected value at line 1 column 1"),
"display still reads as the bare serde error: {display}"
);
}
#[tokio::test]
async fn decode_error_on_html_body_names_content_type_and_snippet() {
let mock = MockServer::start().await;
let html = "<html><body><h1>502 Bad Gateway</h1></body></html>";
Mock::given(method("GET"))
.and(path("/admin/v2/clusters"))
.respond_with(ResponseTemplate::new(200).set_body_raw(html, "text/html"))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let err = admin.cluster_list().await.unwrap_err();
match &err {
AdminError::Decode {
method,
url,
status,
content_type,
snippet,
..
} => {
assert_eq!(method, "GET");
assert!(url.contains("/admin/v2/clusters"), "url was {url}");
assert_eq!(*status, 200);
assert!(
content_type.contains("text/html"),
"content_type was {content_type}"
);
assert!(snippet.contains("502 Bad Gateway"), "snippet was {snippet}");
}
other => panic!("expected AdminError::Decode, got {other:?}"),
}
}
#[tokio::test]
async fn decode_error_on_plain_text_body_names_content_type_and_snippet() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/clusters"))
.respond_with(ResponseTemplate::new(200).set_body_raw("not json", "text/plain"))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let err = admin.cluster_list().await.unwrap_err();
match &err {
AdminError::Decode {
content_type,
snippet,
status,
..
} => {
assert_eq!(*status, 200);
assert!(
content_type.contains("text/plain"),
"content_type was {content_type}"
);
assert!(snippet.contains("not json"), "snippet was {snippet}");
}
other => panic!("expected AdminError::Decode, got {other:?}"),
}
}
#[tokio::test]
async fn decode_error_truncates_long_body_snippet() {
let mock = MockServer::start().await;
let big = format!("<html>{}</html>", "x".repeat(1024));
Mock::given(method("GET"))
.and(path("/admin/v2/clusters"))
.respond_with(ResponseTemplate::new(200).set_body_raw(big, "text/html"))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let err = admin.cluster_list().await.unwrap_err();
match &err {
AdminError::Decode { snippet, .. } => {
assert!(
snippet.contains("… (truncated)"),
"long body should be truncated, snippet was {snippet}"
);
}
other => panic!("expected AdminError::Decode, got {other:?}"),
}
}
#[tokio::test]
async fn no_content_on_or_default_endpoint_yields_default_not_decode() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/namespaces/public/default/retention"))
.respond_with(ResponseTemplate::new(204))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let policy = admin
.namespace_get_retention("public/default")
.await
.expect("204 must fold to the default, not a Decode error");
assert_eq!(policy.retention_time_in_minutes, 0);
assert_eq!(policy.retention_size_in_mb, 0);
}
#[tokio::test]
async fn empty_body_on_or_default_endpoint_yields_default_not_decode() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/namespaces/public/default/retention"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let policy = admin
.namespace_get_retention("public/default")
.await
.expect("empty 200 body must fold to the default, not a Decode error");
assert_eq!(policy.retention_time_in_minutes, 0);
assert_eq!(policy.retention_size_in_mb, 0);
}
#[tokio::test]
async fn null_body_on_or_default_endpoint_yields_default_not_decode() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/namespaces/public/default/retention"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "application/json")
.set_body_string("null"),
)
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let policy = admin
.namespace_get_retention("public/default")
.await
.expect("literal null must fold to the default, not a Decode error");
assert_eq!(policy.retention_time_in_minutes, 0);
assert_eq!(policy.retention_size_in_mb, 0);
}
#[tokio::test]
async fn no_content_on_optional_endpoint_yields_none_not_decode() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/namespaces/public/default/messageTTL"))
.respond_with(ResponseTemplate::new(204))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let ttl = admin
.namespace_get_message_ttl("public/default")
.await
.expect("204 must fold to None, not a Decode error");
assert_eq!(ttl, None);
}
#[tokio::test]
async fn empty_body_on_optional_endpoint_yields_none_not_decode() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/namespaces/public/default/messageTTL"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let ttl = admin
.namespace_get_message_ttl("public/default")
.await
.expect("empty 200 body must fold to None, not a Decode error");
assert_eq!(ttl, None);
}
#[tokio::test]
async fn null_body_on_optional_endpoint_yields_none_not_decode() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/namespaces/public/default/messageTTL"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "application/json")
.set_body_string("null"),
)
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let ttl = admin
.namespace_get_message_ttl("public/default")
.await
.expect("literal null must fold to None, not a Decode error");
assert_eq!(ttl, None);
}
#[tokio::test]
async fn status_error_names_method_and_url() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/admin/v2/clusters"))
.respond_with(ResponseTemplate::new(404).set_body_string("cluster set not found"))
.expect(1)
.mount(&mock)
.await;
let admin = client(&mock);
let err = admin.cluster_list().await.unwrap_err();
match &err {
AdminError::Status {
method,
url,
code,
body,
} => {
assert_eq!(method, "GET");
assert!(url.contains("/admin/v2/clusters"), "url was {url}");
assert_eq!(*code, 404);
assert!(body.contains("cluster set not found"), "body was {body}");
}
other => panic!("expected AdminError::Status, got {other:?}"),
}
let display = err.to_string();
assert!(display.contains("GET"), "display: {display}");
assert!(display.contains("/admin/v2/clusters"), "display: {display}");
assert!(display.contains("404"), "display: {display}");
}