use super::*;
#[test]
fn api_errors_redact_tokens() {
let error = api_error(
StatusCode::UNAUTHORIZED,
"/api/v1/clients".to_string(),
r#"{"message":"token secret-token is bad"}"#.to_string(),
"secret-token",
);
let message = error.to_string();
assert!(message.contains("[REDACTED]"), "got: {message}");
assert!(!message.contains("secret-token"), "got: {message}");
}
#[test]
fn api_errors_extract_arrays_objects_and_plain_text() {
let array_error = api_error(
StatusCode::UNPROCESSABLE_ENTITY,
"/api/v1/clients".to_string(),
r#"{"errors":["name is required",{"email":"invalid"}]}"#.to_string(),
"token",
);
assert!(array_error.to_string().contains("name is required"));
let object_error = api_error(
StatusCode::BAD_REQUEST,
"/api/v1/clients".to_string(),
r#"{"error":{"message":"bad"}}"#.to_string(),
"token",
);
assert!(object_error.to_string().contains("message"));
let text_error = api_error(
StatusCode::INTERNAL_SERVER_ERROR,
"/api/v1/clients".to_string(),
"plain failure".to_string(),
"token",
);
assert!(text_error.to_string().contains("plain failure"));
let numeric_message = api_error(
StatusCode::BAD_REQUEST,
"/api/v1/clients".to_string(),
r#"{"message":123}"#.to_string(),
"token",
);
assert!(numeric_message.to_string().contains("\"message\":123"));
}
#[tokio::test]
async fn get_json_reports_transport_errors() {
let client =
ApiClient::new(Config::from_values("http://127.0.0.1:9", "secret-token").expect("config"));
let error = client
.get_json("api/v1/statics", &[])
.await
.expect_err("transport failure");
let message = error.to_string();
assert!(matches!(error, KobanError::Transport { .. }));
assert!(!message.contains("secret-token"), "got: {message}");
}
#[tokio::test]
async fn json_write_methods_report_decode_api_and_transport_errors() {
let server = MockServer::start();
let invalid_json = server.mock(|when, then| {
when.method(POST).path("/api/v1/invoices");
then.status(200).body("not json");
});
let api_failure = server.mock(|when, then| {
when.method(PUT).path("/api/v1/invoices/invoice_1");
then.status(422).body(r#"{"message":"bad invoice"}"#);
});
let client = ApiClient::new(Config::from_values(server.base_url(), "token").expect("config"));
let decode = client
.post_json("api/v1/invoices", &[], &serde_json::json!({}))
.await
.expect_err("invalid JSON");
assert!(matches!(decode, KobanError::Decode { .. }));
invalid_json.assert();
let api = client
.put_json("api/v1/invoices/invoice_1", &[], &serde_json::json!({}))
.await
.expect_err("API failure");
assert!(matches!(api, KobanError::Api { .. }));
let api_message = api.to_string();
assert!(api_message.contains("bad invoice"));
assert!(api_message.contains(&server.base_url()));
assert!(api_message.contains("/api/v1/invoices/invoice_1"));
api_failure.assert();
let userinfo_failure = server.mock(|when, then| {
when.method(GET).path("/api/v1/invoices");
then.status(403).body(r#"{"message":"denied"}"#);
});
let mut userinfo_url = url::Url::parse(&server.base_url()).expect("server URL");
userinfo_url.set_username("user").expect("set username");
userinfo_url
.set_password(Some("pass"))
.expect("set password");
let userinfo_server =
ApiClient::new(Config::from_values(userinfo_url.as_str(), "token").expect("config"));
let userinfo = userinfo_server
.get_json("api/v1/invoices", &[("page".to_string(), "1".to_string())])
.await
.expect_err("userinfo API failure");
let userinfo_message = userinfo.to_string();
assert!(matches!(userinfo, KobanError::Api { .. }));
assert!(
userinfo_message.contains("page=1"),
"got: {userinfo_message}"
);
assert!(
!userinfo_message.contains("user:pass"),
"got: {userinfo_message}"
);
userinfo_failure.assert();
let offline =
ApiClient::new(Config::from_values("http://127.0.0.1:9", "secret-token").expect("config"));
let transport = offline
.delete_json("api/v1/invoices/invoice_1", &[])
.await
.expect_err("transport failure");
assert!(matches!(transport, KobanError::Transport { .. }));
assert!(!transport.to_string().contains("secret-token"));
}
#[tokio::test]
async fn json_write_methods_redact_transport_errors() {
let client =
ApiClient::new(Config::from_values("http://127.0.0.1:9", "secret-token").expect("config"));
let post = client
.post_json("api/v1/invoices", &[], &serde_json::json!({}))
.await
.expect_err("post transport");
let put = client
.put_json("api/v1/invoices/invoice_1", &[], &serde_json::json!({}))
.await
.expect_err("put transport");
for error in [post, put] {
assert!(matches!(error, KobanError::Transport { .. }));
assert!(!error.to_string().contains("secret-token"));
}
}
#[tokio::test]
async fn multipart_upload_reports_api_failure_after_reading_files() {
let tempdir = tempfile::tempdir().expect("tempdir");
let upload = tempdir.path().join("upload.txt");
std::fs::write(&upload, b"document").expect("upload file");
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(PUT).path("/api/v1/invoices/invoice_1/upload");
then.status(400).body(r#"{"message":"upload rejected"}"#);
});
let client = ApiClient::new(Config::from_values(server.base_url(), "token").expect("config"));
let error = client
.put_multipart("api/v1/invoices/invoice_1/upload", &[], &[upload])
.await
.expect_err("upload failure");
assert!(matches!(error, KobanError::Api { .. }));
assert!(error.to_string().contains("upload rejected"));
mock.assert();
}
#[tokio::test]
async fn multipart_upload_redacts_transport_errors_after_reading_files() {
let tempdir = tempfile::tempdir().expect("tempdir");
let upload = tempdir.path().join("upload.txt");
std::fs::write(&upload, b"document").expect("upload file");
let client =
ApiClient::new(Config::from_values("http://127.0.0.1:9", "secret-token").expect("config"));
let error = client
.put_multipart("api/v1/invoices/invoice_1/upload", &[], &[upload])
.await
.expect_err("transport failure");
assert!(matches!(error, KobanError::Transport { .. }));
assert!(!error.to_string().contains("secret-token"));
}
#[tokio::test]
async fn get_bytes_reports_api_and_transport_errors() {
let server = MockServer::start();
let failing_download = server.mock(|when, then| {
when.method(GET).path("/api/v1/invoice/invitation/download");
then.status(404)
.body(r#"{"message":"missing secret-token"}"#);
});
let client =
ApiClient::new(Config::from_values(server.base_url(), "secret-token").expect("config"));
let error = client
.get_bytes("api/v1/invoice/invitation/download", &[])
.await
.expect_err("api failure");
let message = error.to_string();
assert!(matches!(error, KobanError::Api { .. }));
assert!(message.contains("[REDACTED]"), "got: {message}");
assert!(!message.contains("secret-token"), "got: {message}");
failing_download.assert();
let client =
ApiClient::new(Config::from_values("http://127.0.0.1:9", "secret-token").expect("config"));
let error = client
.get_bytes("api/v1/invoice/invitation/download", &[])
.await
.expect_err("transport failure");
assert!(matches!(error, KobanError::Transport { .. }));
}
#[tokio::test]
async fn get_bytes_returns_success_bytes() {
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(GET).path("/api/v1/invoice/invitation/download");
then.status(200).body("pdf bytes");
});
let client = ApiClient::new(Config::from_values(server.base_url(), "token").expect("config"));
let bytes = client
.get_bytes("api/v1/invoice/invitation/download", &[])
.await
.expect("bytes");
assert_eq!(bytes, b"pdf bytes");
mock.assert();
}
#[tokio::test]
async fn get_json_reports_decode_errors() {
let server = MockServer::start();
let invalid_json = server.mock(|when, then| {
when.method(GET).path("/api/v1/statics");
then.status(200).body("not json");
});
let client = ApiClient::new(Config::from_values(server.base_url(), "token").expect("config"));
let error = client
.get_json("api/v1/statics", &[])
.await
.expect_err("decode failure");
assert!(matches!(error, KobanError::Decode { .. }));
invalid_json.assert();
}