use mockito::{Matcher, Server};
use std::time::Duration;
use tempfile::NamedTempFile;
async fn create_test_client(server_url: String) -> otelite_client::ApiClient {
otelite_client::ApiClient::new(server_url, Duration::from_secs(30)).unwrap()
}
fn create_test_config(
endpoint: String,
format: otelite::config::OutputFormat,
) -> otelite::config::Config {
otelite::config::Config {
endpoint,
timeout: Duration::from_secs(30),
format,
no_color: true, no_header: false,
no_pager: true,
}
}
#[tokio::test]
async fn test_traces_list_command() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces")
.match_query(Matcher::UrlEncoded("limit".into(), "10".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"traces": [
{
"trace_id": "trace1",
"root_span_name": "http-request",
"start_time": 1705315800000000000,
"duration": 1500000000,
"span_count": 1,
"service_names": [],
"has_errors": false
},
{
"trace_id": "trace2",
"root_span_name": "database-query",
"start_time": 1705315860000000000,
"duration": 250000000,
"span_count": 1,
"service_names": [],
"has_errors": false
}
], "total": 2, "limit": 10, "offset": 0}"#,
)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result =
otelite::commands::traces::handle_list(&client, &config, Some(10), None, None, None).await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_traces_list_empty() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"traces": [], "total": 0, "limit": 100, "offset": 0}"#)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result =
otelite::commands::traces::handle_list(&client, &config, None, None, None, None).await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_traces_show_command() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces/trace123")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{
"trace_id": "trace123",
"spans": [
{
"span_id": "span1",
"trace_id": "trace123",
"parent_span_id": null,
"name": "http-request",
"kind": "Internal",
"start_time": 1705315800000000000,
"end_time": 1705315801500000000,
"duration": 1500000000,
"attributes": {"http.method": "GET"},
"resource": null,
"status": {"code": "OK", "message": null},
"events": []
},
{
"span_id": "span2",
"trace_id": "trace123",
"parent_span_id": "span1",
"name": "database-query",
"kind": "Internal",
"start_time": 1705315800100000000,
"end_time": 1705315800350000000,
"duration": 250000000,
"attributes": {"db.system": "postgresql"},
"resource": null,
"status": {"code": "OK", "message": null},
"events": []
}
],
"start_time": 1705315800000000000,
"end_time": 1705315801500000000,
"duration": 1500000000,
"span_count": 2,
"service_names": []
}"#,
)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result = otelite::commands::traces::handle_show(&client, &config, "trace123").await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_traces_show_not_found() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces/nonexistent")
.with_status(404)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result = otelite::commands::traces::handle_show(&client, &config, "nonexistent").await;
mock.assert_async().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_traces_list_with_duration_filter() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces")
.match_query(Matcher::UrlEncoded("min_duration".into(), "1000".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"traces": [
{
"trace_id": "trace1",
"root_span_name": "slow-request",
"start_time": 1705315800000000000,
"duration": 2000000000,
"span_count": 1,
"service_names": [],
"has_errors": false
}
], "total": 1, "limit": 100, "offset": 0}"#,
)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result =
otelite::commands::traces::handle_list(&client, &config, None, Some(1000), None, None)
.await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_traces_list_with_status_filter() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces")
.match_query(Matcher::UrlEncoded("status".into(), "ERROR".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"traces": [
{
"trace_id": "trace1",
"root_span_name": "failed-request",
"start_time": 1705315800000000000,
"duration": 500000000,
"span_count": 1,
"service_names": [],
"has_errors": true
}
], "total": 1, "limit": 100, "offset": 0}"#,
)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result = otelite::commands::traces::handle_list(
&client,
&config,
None,
None,
Some("ERROR".to_string()),
None,
)
.await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_traces_show_with_span_tree() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces/trace123")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{
"trace_id": "trace123",
"spans": [
{"span_id": "span1", "trace_id": "trace123", "parent_span_id": null, "name": "http-request", "kind": "Internal", "start_time": 1705315800000000000, "end_time": 1705315801500000000, "duration": 1500000000, "attributes": {}, "resource": null, "status": {"code": "OK", "message": null}, "events": []},
{"span_id": "span2", "trace_id": "trace123", "parent_span_id": "span1", "name": "middleware", "kind": "Internal", "start_time": 1705315800100000000, "end_time": 1705315801100000000, "duration": 1000000000, "attributes": {}, "resource": null, "status": {"code": "OK", "message": null}, "events": []},
{"span_id": "span3", "trace_id": "trace123", "parent_span_id": "span2", "name": "handler", "kind": "Internal", "start_time": 1705315800200000000, "end_time": 1705315801000000000, "duration": 800000000, "attributes": {}, "resource": null, "status": {"code": "OK", "message": null}, "events": []},
{"span_id": "span4", "trace_id": "trace123", "parent_span_id": "span3", "name": "database-query", "kind": "Internal", "start_time": 1705315800300000000, "end_time": 1705315800550000000, "duration": 250000000, "attributes": {}, "resource": null, "status": {"code": "OK", "message": null}, "events": []}
],
"start_time": 1705315800000000000,
"end_time": 1705315801500000000,
"duration": 1500000000,
"span_count": 4,
"service_names": []
}"#,
)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Pretty);
let result = otelite::commands::traces::handle_show(&client, &config, "trace123").await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_traces_json_output_with_spans() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces/trace123")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{
"trace_id": "trace123",
"spans": [
{"span_id": "span1", "trace_id": "trace123", "parent_span_id": null, "name": "http-request", "kind": "Internal", "start_time": 1705315800000000000, "end_time": 1705315801500000000, "duration": 1500000000, "attributes": {"http.method": "GET", "http.url": "/api/users"}, "resource": null, "status": {"code": "OK", "message": null}, "events": []},
{"span_id": "span2", "trace_id": "trace123", "parent_span_id": "span1", "name": "database-query", "kind": "Internal", "start_time": 1705315800100000000, "end_time": 1705315800350000000, "duration": 250000000, "attributes": {"db.system": "postgresql", "db.statement": "SELECT * FROM users"}, "resource": null, "status": {"code": "OK", "message": null}, "events": []}
],
"start_time": 1705315800000000000,
"end_time": 1705315801500000000,
"duration": 1500000000,
"span_count": 2,
"service_names": []
}"#,
)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result = otelite::commands::traces::handle_show(&client, &config, "trace123").await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_traces_list_pretty_output() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/api/traces")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"traces": [
{
"trace_id": "trace1",
"root_span_name": "http-request",
"start_time": 1705315800000000000,
"duration": 1500000000,
"span_count": 1,
"service_names": [],
"has_errors": false
}
], "total": 1, "limit": 100, "offset": 0}"#,
)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Pretty);
let result =
otelite::commands::traces::handle_list(&client, &config, None, None, None, None).await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_traces_export_json_stdout_is_valid_json_array() {
let mut server = Server::new_async().await;
let body = r#"[
{
"trace_id": "trace-123",
"spans": [
{
"span_id": "span-1",
"trace_id": "trace-123",
"parent_span_id": null,
"name": "root-span",
"kind": "Internal",
"start_time": 1705315800000000000,
"end_time": 1705315801500000000,
"duration": 1500000000,
"attributes": {},
"resource": null,
"status": {"code": "OK", "message": null},
"events": []
}
],
"start_time": 1705315800000000000,
"end_time": 1705315801500000000,
"duration": 1500000000,
"span_count": 1,
"service_names": ["api"]
}
]"#;
let mock = server
.mock("GET", "/api/traces/export")
.match_query(Matcher::UrlEncoded("format".into(), "json".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result =
otelite::commands::traces::handle_export(&client, &config, "json", None, None, None, None)
.await;
mock.assert_async().await;
assert!(result.is_ok());
let parsed: Vec<serde_json::Value> = serde_json::from_str(body).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["trace_id"], "trace-123");
assert_eq!(parsed[0]["spans"].as_array().unwrap().len(), 1);
}
#[tokio::test]
async fn test_traces_export_json_file_output_writes_valid_json() {
let mut server = Server::new_async().await;
let body = r#"[
{
"trace_id": "trace-file",
"spans": [],
"start_time": 1705315800000000000,
"end_time": 1705315801500000000,
"duration": 1500000000,
"span_count": 0,
"service_names": []
}
]"#;
let mock = server
.mock("GET", "/api/traces/export")
.match_query(Matcher::UrlEncoded("format".into(), "json".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let file = NamedTempFile::new().unwrap();
let path = file.path().to_string_lossy().to_string();
let result = otelite::commands::traces::handle_export(
&client,
&config,
"json",
None,
None,
None,
Some(path.clone()),
)
.await;
mock.assert_async().await;
assert!(result.is_ok());
let written = std::fs::read_to_string(&path).unwrap();
let parsed: Vec<serde_json::Value> = serde_json::from_str(&written).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["trace_id"], "trace-file");
}
#[tokio::test]
async fn test_traces_export_with_data_includes_trace_id_and_spans() {
let mut server = Server::new_async().await;
let body = r#"[
{
"trace_id": "trace-data",
"spans": [
{
"span_id": "span-a",
"trace_id": "trace-data",
"parent_span_id": null,
"name": "span-a",
"kind": "Internal",
"start_time": 1705315800000000000,
"end_time": 1705315800100000000,
"duration": 100000000,
"attributes": {},
"resource": null,
"status": {"code": "OK", "message": null},
"events": []
}
],
"start_time": 1705315800000000000,
"end_time": 1705315800100000000,
"duration": 100000000,
"span_count": 1,
"service_names": []
}
]"#;
let mock = server
.mock("GET", "/api/traces/export")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("format".into(), "json".into()),
Matcher::UrlEncoded("status".into(), "ERROR".into()),
Matcher::UrlEncoded("min_duration".into(), "250".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result = otelite::commands::traces::handle_export(
&client,
&config,
"json",
Some("ERROR".to_string()),
Some(250),
None,
None,
)
.await;
mock.assert_async().await;
assert!(result.is_ok());
let parsed: Vec<serde_json::Value> = serde_json::from_str(body).unwrap();
assert_eq!(parsed[0]["trace_id"], "trace-data");
assert_eq!(
parsed[0]["spans"].as_array().unwrap()[0]["span_id"],
"span-a"
);
}
#[tokio::test]
async fn test_traces_export_empty_result_is_empty_array() {
let mut server = Server::new_async().await;
let body = "[]";
let mock = server
.mock("GET", "/api/traces/export")
.match_query(Matcher::UrlEncoded("format".into(), "json".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create_async()
.await;
let client = create_test_client(server.url()).await;
let config = create_test_config(server.url(), otelite::config::OutputFormat::Json);
let result =
otelite::commands::traces::handle_export(&client, &config, "json", None, None, None, None)
.await;
mock.assert_async().await;
assert!(result.is_ok());
let parsed: Vec<serde_json::Value> = serde_json::from_str(body).unwrap();
assert!(parsed.is_empty());
}