use assert_cmd::Command;
use mockito::Server;
use predicates::prelude::*;
use std::net::TcpListener;
fn otelite() -> Command {
Command::cargo_bin("otelite").expect("otelite binary should build")
}
fn closed_port() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind a local listener");
let port = listener.local_addr().unwrap().port();
drop(listener);
port
}
#[test]
fn test_cli_invalid_endpoint_explains_error() {
let mut cmd = otelite();
cmd.args(["--endpoint", "not-a-url", "logs", "list"]);
cmd.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("Invalid --endpoint 'not-a-url'"))
.stderr(predicate::str::contains("expected a URL"));
}
#[test]
fn test_cli_zero_timeout_is_error() {
let mut cmd = otelite();
cmd.args([
"--timeout",
"0",
"--endpoint",
"http://127.0.0.1:1",
"logs",
"list",
]);
cmd.assert()
.failure()
.code(1)
.stderr(predicate::str::contains(
"timeout must be at least 1 second",
));
}
#[test]
fn test_cli_negative_timeout_is_rejected() {
let mut cmd = otelite();
cmd.args(["--timeout=-1", "logs", "list"]);
cmd.assert()
.failure()
.code(2)
.stderr(predicate::str::contains(
"invalid value '-1' for '--timeout <TIMEOUT>'",
));
}
#[test]
fn test_cli_connection_refused_exit_2() {
let port = closed_port();
let mut cmd = otelite();
cmd.args([
"--endpoint",
&format!("http://127.0.0.1:{}", port),
"logs",
"list",
]);
cmd.assert()
.failure()
.code(2)
.stderr(predicate::str::contains(
"Failed to connect to Otelite backend",
));
}
async fn start_silent_server() -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind silent server");
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
while let Ok((socket, _)) = listener.accept().await {
tokio::spawn(async move {
let _ = socket;
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
});
}
});
format!("http://{}", addr)
}
#[tokio::test]
async fn test_cli_request_timeout_exit_2() {
let endpoint = start_silent_server().await;
let mut cmd = otelite();
cmd.args([
"--endpoint",
endpoint.as_str(),
"--timeout",
"1",
"logs",
"list",
]);
cmd.assert()
.failure()
.code(2)
.stderr(predicate::str::contains(
"Failed to connect to Otelite backend",
));
}
#[tokio::test]
async fn test_cli_api_404_exit_3() {
let mut server = Server::new_async().await;
server
.mock("GET", "/api/logs")
.match_query(mockito::Matcher::Any)
.with_status(404)
.with_body("not found")
.create_async()
.await;
let mut cmd = otelite();
cmd.args(["--endpoint", server.url().as_str(), "logs", "list"]);
cmd.assert()
.failure()
.code(3)
.stderr(predicate::str::contains("resource not found"));
}
#[tokio::test]
async fn test_cli_api_500_exit_1_readable() {
let mut server = Server::new_async().await;
server
.mock("GET", "/api/logs")
.match_query(mockito::Matcher::Any)
.with_status(500)
.with_body("internal error")
.create_async()
.await;
let mut cmd = otelite();
cmd.args(["--endpoint", server.url().as_str(), "logs", "list"]);
cmd.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("HTTP 500"));
}
#[tokio::test]
async fn test_cli_malformed_json_exit_1_no_panic() {
let mut server = Server::new_async().await;
server
.mock("GET", "/api/logs")
.match_query(mockito::Matcher::Any)
.with_status(200)
.with_header("content-type", "application/json")
.with_body("this is not json {")
.create_async()
.await;
let mut cmd = otelite();
cmd.args(["--endpoint", server.url().as_str(), "logs", "list"]);
cmd.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("error decoding response body"))
.stderr(predicate::str::contains("panic").not());
}
#[tokio::test]
async fn test_cli_limit_zero_returns_empty() {
let mut server = Server::new_async().await;
server
.mock("GET", "/api/logs")
.match_query(mockito::Matcher::Any)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"logs": [], "total": 0, "limit": 0, "offset": 0}"#)
.create_async()
.await;
let mut cmd = otelite();
cmd.args([
"--endpoint",
server.url().as_str(),
"logs",
"list",
"--limit",
"0",
]);
cmd.assert().success();
}
fn run_usage(args: &[&str]) -> (assert_cmd::assert::Assert, tempfile::TempDir) {
let tmp = tempfile::tempdir().expect("temp dir");
let mut cmd = otelite();
cmd.args(args).env("OTELITE_DATA_DIR", tmp.path());
(cmd.assert(), tmp)
}
#[test]
fn test_usage_empty_database() {
let (assert, _tmp) = run_usage(&["usage"]);
assert.success();
}
#[test]
fn test_usage_since_24h() {
let (assert, _tmp) = run_usage(&["usage", "--since", "24h"]);
assert.success();
}
#[test]
fn test_usage_json_is_valid_json() {
let (assert, _tmp) = run_usage(&["usage", "--format", "json"]);
let stdout = assert.success().get_output().stdout.clone();
let json = std::str::from_utf8(&stdout).expect("utf-8 stdout");
serde_json::from_str::<serde_json::Value>(json)
.expect("usage --format json must emit valid JSON");
}