mirror of
https://github.com/coralogix/cx-cli.git
synced 2026-09-14 16:15:45 +08:00
229d2d34fb
AIAG-980
347 lines
10 KiB
Rust
347 lines
10 KiB
Rust
use clap::{CommandFactory, Parser, Subcommand};
|
|
use serde_json::{json, Value};
|
|
use wiremock::matchers::{header, method, path};
|
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
|
|
use coralogix_cli::api_client::CxClient;
|
|
use coralogix_cli::config::OutputFormat;
|
|
use coralogix_cli::request_metadata::RequestMetadata;
|
|
|
|
fn init_tls() {
|
|
rustls::crypto::ring::default_provider()
|
|
.install_default()
|
|
.ok();
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
struct TestCli {
|
|
#[command(subcommand)]
|
|
command: TestCommand,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum TestCommand {
|
|
Logs,
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn attaches_cli_request_metadata_headers() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
let sdk_version = concat!("cx-cli-", env!("CARGO_PKG_VERSION"));
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.and(header("x-cx-sdk-version", sdk_version))
|
|
.and(header("x-cx-cli-command-path", "logs"))
|
|
.and(header("x-cx-cli-output-format", "agents"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let matches = TestCli::command()
|
|
.try_get_matches_from(["cx", "logs"])
|
|
.unwrap();
|
|
let metadata = RequestMetadata::from_invocation(&matches, OutputFormat::Agents, &[], false);
|
|
let client = CxClient::new_with_metadata(server.uri(), "test-key", &metadata).unwrap();
|
|
|
|
client.get::<Value>("/test", &[]).await.unwrap();
|
|
|
|
let requests = server.received_requests().await.unwrap();
|
|
assert_eq!(requests.len(), 1);
|
|
assert!(requests[0]
|
|
.headers
|
|
.get("x-cx-cli-installation-id")
|
|
.is_some());
|
|
assert!(requests[0].headers.get("x-cx-cli-metadata").is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_401_unauthorized() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(ResponseTemplate::new(401))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("Invalid or expired API key"),
|
|
"expected auth error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_403_with_message() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(
|
|
ResponseTemplate::new(403)
|
|
.set_body_json(json!({"message": "missing dashboards:write scope"})),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("missing dashboards:write scope"),
|
|
"expected server message in error, got: {msg}"
|
|
);
|
|
assert!(
|
|
!msg.contains("Check your API key's scopes"),
|
|
"scope hint should not be appended when server provides detail, got: {msg}"
|
|
);
|
|
assert!(
|
|
msg.contains("Permission denied"),
|
|
"expected permission error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_403_quota_exceeded() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(
|
|
ResponseTemplate::new(403).set_body_json(json!({"message": "quota exceeded"})),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("quota exceeded"),
|
|
"expected server message in error, got: {msg}"
|
|
);
|
|
assert!(
|
|
!msg.contains("Check your API key's scopes"),
|
|
"scope hint should not be appended for quota errors, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_403_without_message() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(ResponseTemplate::new(403))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("You do not have permission for this operation"),
|
|
"expected generic permission error, got: {msg}"
|
|
);
|
|
assert!(
|
|
msg.contains("Check your API key's scopes"),
|
|
"expected permission hint, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_429_with_retry_after() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(ResponseTemplate::new(429).append_header("Retry-After", "30"))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("Retry after 30 seconds"),
|
|
"expected retry-after in error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_429_without_retry_after() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(ResponseTemplate::new(429))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("Wait and retry"),
|
|
"expected generic rate-limit error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_500_with_json_message() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(
|
|
ResponseTemplate::new(500).set_body_json(json!({"message": "internal error"})),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("internal error"),
|
|
"expected server message in error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_401_with_server_message() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(ResponseTemplate::new(401).set_body_json(json!({"message": "Token expired"})))
|
|
.mount(&server)
|
|
.await;
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("Token expired"),
|
|
"expected server message, got: {msg}"
|
|
);
|
|
assert!(msg.contains("cx profiles add"), "expected hint, got: {msg}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_429_with_message_and_retry_after() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(
|
|
ResponseTemplate::new(429)
|
|
.append_header("Retry-After", "30")
|
|
.set_body_json(json!({"message": "Per-team quota exhausted"})),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("Per-team quota exhausted"),
|
|
"expected server message, got: {msg}"
|
|
);
|
|
assert!(
|
|
msg.contains("Retry after 30 seconds"),
|
|
"expected retry hint, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_404_catch_all() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(
|
|
ResponseTemplate::new(404).set_body_json(json!({"message": "resource not found"})),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("resource not found"),
|
|
"expected server message, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_401_with_nested_error_message() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(
|
|
ResponseTemplate::new(401)
|
|
.set_body_json(json!({"error": {"message": "Bearer prefix missing"}})),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("Bearer prefix missing"),
|
|
"expected nested server message, got: {msg}"
|
|
);
|
|
assert!(msg.contains("cx profiles add"), "expected hint, got: {msg}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_500_with_raw_body() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.respond_with(ResponseTemplate::new(500).set_body_string("something went wrong"))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let err = client.get::<Value>("/test", &[]).await.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("something went wrong"),
|
|
"expected raw body in error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sdk_version_header_is_sent() {
|
|
init_tls();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/test"))
|
|
.and(header(
|
|
"x-cx-sdk-version",
|
|
concat!("cx-cli-", env!("CARGO_PKG_VERSION")),
|
|
))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = CxClient::new(server.uri(), "test-key").unwrap();
|
|
let _: Value = client.get("/test", &[]).await.unwrap();
|
|
}
|