use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn setup_config(dir: &TempDir, config_toml: &str) {
let cinch_dir = dir.path().join(".cinch");
fs::create_dir_all(&cinch_dir).expect("create .cinch dir");
fs::write(cinch_dir.join("config.toml"), config_toml).expect("write config");
}
fn cinch_cmd(home: &TempDir, api_url: &str) -> Command {
let mut cmd = Command::cargo_bin("cinch").expect("binary exists");
cmd.env("HOME", home.path());
cmd.env("CINCH_API_URL", api_url);
cmd.env_remove("CINCH_API_KEY");
cmd
}
#[test]
fn test_help_output() {
Command::cargo_bin("cinch")
.expect("binary exists")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("CinchDB"))
.stdout(predicate::str::contains("auth"))
.stdout(predicate::str::contains("db"))
.stdout(predicate::str::contains("scope"))
.stdout(predicate::str::contains("org"))
.stdout(predicate::str::contains("config"))
.stdout(predicate::str::contains("env"));
}
#[test]
fn test_version_output() {
Command::cargo_bin("cinch")
.expect("binary exists")
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains("cinch"));
}
#[test]
fn test_auth_help() {
Command::cargo_bin("cinch")
.expect("binary exists")
.args(["auth", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("login"))
.stdout(predicate::str::contains("logout"))
.stdout(predicate::str::contains("whoami"))
.stdout(predicate::str::contains("api-keys"));
}
#[test]
fn test_db_help() {
Command::cargo_bin("cinch")
.expect("binary exists")
.args(["db", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("create"))
.stdout(predicate::str::contains("list"))
.stdout(predicate::str::contains("show"))
.stdout(predicate::str::contains("shell"))
.stdout(predicate::str::contains("destroy"))
.stdout(predicate::str::contains("wake"))
.stdout(predicate::str::contains("token"));
}
#[test]
fn test_scope_help() {
Command::cargo_bin("cinch")
.expect("binary exists")
.args(["scope", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("create"))
.stdout(predicate::str::contains("list"))
.stdout(predicate::str::contains("destroy"))
.stdout(predicate::str::contains("token"));
}
#[test]
fn test_config_help() {
Command::cargo_bin("cinch")
.expect("binary exists")
.args(["config", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("set"))
.stdout(predicate::str::contains("path"));
}
#[test]
fn test_unknown_subcommand() {
Command::cargo_bin("cinch")
.expect("binary exists")
.arg("nonexistent")
.assert()
.failure()
.stderr(predicate::str::contains("unrecognized subcommand"));
}
#[test]
fn test_config_show_empty() {
let home = TempDir::new().expect("tempdir");
cinch_cmd(&home, "http://localhost:0")
.args(["config"])
.assert()
.success()
.stdout(predicate::str::contains("(not set)"));
}
#[test]
fn test_config_show_with_context() {
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
r#"
[context]
org = "acme"
project = "api"
environment = "production"
scope = "default"
"#,
);
cinch_cmd(&home, "http://localhost:0")
.args(["config"])
.assert()
.success()
.stdout(predicate::str::contains("acme"))
.stdout(predicate::str::contains("api"))
.stdout(predicate::str::contains("production"))
.stdout(predicate::str::contains("default"));
}
#[test]
fn test_config_show_json() {
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
r#"
[context]
org = "acme"
"#,
);
let output = cinch_cmd(&home, "http://localhost:0")
.args(["--json", "config"])
.output()
.expect("run");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid json");
assert_eq!(json["context"]["org"], "acme");
assert_eq!(json["authenticated"], false);
}
#[test]
fn test_config_path() {
let home = TempDir::new().expect("tempdir");
cinch_cmd(&home, "http://localhost:0")
.args(["config", "path"])
.assert()
.success()
.stdout(predicate::str::contains(".cinch/config.toml"));
}
#[test]
fn test_config_path_json() {
let home = TempDir::new().expect("tempdir");
let output = cinch_cmd(&home, "http://localhost:0")
.args(["--json", "config", "path"])
.output()
.expect("run");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid json");
assert!(json["path"].as_str().expect("path string").contains(".cinch/config.toml"));
}
#[test]
fn test_config_set() {
let home = TempDir::new().expect("tempdir");
cinch_cmd(&home, "http://localhost:0")
.args(["config", "set", "org", "myorg"])
.assert()
.success()
.stdout(predicate::str::contains("Set org = \"myorg\""));
let config_path = home.path().join(".cinch").join("config.toml");
let content = fs::read_to_string(config_path).expect("read config");
assert!(content.contains("myorg"));
}
#[test]
fn test_config_set_json() {
let home = TempDir::new().expect("tempdir");
let output = cinch_cmd(&home, "http://localhost:0")
.args(["--json", "config", "set", "org", "testorg"])
.output()
.expect("run");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid json");
assert_eq!(json["key"], "org");
assert_eq!(json["value"], "testorg");
assert_eq!(json["status"], "set");
}
#[test]
fn test_config_set_multiple_keys() {
let home = TempDir::new().expect("tempdir");
for (key, value) in [
("org", "acme"),
("project", "api"),
("environment", "staging"),
("scope", "default"),
("api.url", "https://custom.api.dev"),
] {
cinch_cmd(&home, "http://localhost:0")
.args(["config", "set", key, value])
.assert()
.success();
}
let output = cinch_cmd(&home, "http://localhost:0")
.args(["--json", "config"])
.output()
.expect("run");
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid json");
assert_eq!(json["context"]["org"], "acme");
assert_eq!(json["context"]["project"], "api");
assert_eq!(json["context"]["environment"], "staging");
assert_eq!(json["context"]["scope"], "default");
assert_eq!(json["api"]["url"], "https://custom.api.dev");
}
#[test]
fn test_config_set_invalid_key() {
let home = TempDir::new().expect("tempdir");
cinch_cmd(&home, "http://localhost:0")
.args(["config", "set", "bogus", "value"])
.assert()
.failure()
.stderr(predicate::str::contains("unknown config key"));
}
#[tokio::test]
async fn test_whoami_not_logged_in() {
let home = TempDir::new().expect("tempdir");
cinch_cmd(&home, "http://localhost:0")
.args(["auth", "whoami"])
.assert()
.failure()
.stderr(predicate::str::contains("not logged in"));
}
#[tokio::test]
async fn test_whoami_with_token() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users/me"))
.and(header("Authorization", "Bearer test-jwt-token"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "usr_abc123",
"email": "alice@example.com",
})))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
&format!(
r#"
[auth]
token = "test-jwt-token"
[context]
org = "acme"
project = "api"
environment = "prod"
scope = "default"
"#
),
);
cinch_cmd(&home, &mock_server.uri())
.args(["auth", "whoami"])
.assert()
.success()
.stdout(predicate::str::contains("alice@example.com"))
.stdout(predicate::str::contains("usr_abc123"))
.stdout(predicate::str::contains("acme"))
.stdout(predicate::str::contains("api"));
}
#[tokio::test]
async fn test_whoami_json() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users/me"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "usr_abc123",
"email": "alice@example.com",
})))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
r#"
[auth]
token = "test-jwt-token"
[context]
org = "acme"
"#,
);
let output = cinch_cmd(&home, &mock_server.uri())
.args(["--json", "auth", "whoami"])
.output()
.expect("run");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid json");
assert_eq!(json["user"]["email"], "alice@example.com");
assert_eq!(json["user"]["id"], "usr_abc123");
assert_eq!(json["context"]["org"], "acme");
}
#[tokio::test]
async fn test_whoami_unauthorized() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users/me"))
.respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({
"error": "invalid or expired token",
})))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
r#"
[auth]
token = "expired-token"
"#,
);
cinch_cmd(&home, &mock_server.uri())
.args(["auth", "whoami"])
.assert()
.failure()
.stderr(predicate::str::contains("not authenticated"));
}
#[tokio::test]
async fn test_logout_clears_credentials() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/auth/logout"))
.respond_with(ResponseTemplate::new(200))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
r#"
[auth]
token = "test-jwt-token"
api_key = "ck_live_testkey"
[context]
org = "acme"
"#,
);
cinch_cmd(&home, &mock_server.uri())
.args(["auth", "logout"])
.assert()
.success()
.stdout(predicate::str::contains("Logged out"));
let config_path = home.path().join(".cinch").join("config.toml");
let content = fs::read_to_string(config_path).expect("read config");
assert!(!content.contains("test-jwt-token"));
assert!(!content.contains("ck_live_testkey"));
assert!(content.contains("acme")); }
#[tokio::test]
async fn test_logout_json() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/auth/logout"))
.respond_with(ResponseTemplate::new(200))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
r#"
[auth]
token = "test-jwt-token"
"#,
);
let output = cinch_cmd(&home, &mock_server.uri())
.args(["--json", "auth", "logout"])
.output()
.expect("run");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid json");
assert_eq!(json["status"], "logged_out");
}
#[tokio::test]
async fn test_login_headless_invalid_key_format() {
let home = TempDir::new().expect("tempdir");
cinch_cmd(&home, "http://localhost:0")
.args(["auth", "login", "--headless"])
.write_stdin("not_a_valid_key\n")
.assert()
.failure()
.stderr(predicate::str::contains("invalid API key format"));
}
#[tokio::test]
async fn test_login_headless_empty_key() {
let home = TempDir::new().expect("tempdir");
cinch_cmd(&home, "http://localhost:0")
.args(["auth", "login", "--headless"])
.write_stdin("\n")
.assert()
.failure()
.stderr(predicate::str::contains("no API key provided"));
}
#[tokio::test]
async fn test_login_already_logged_in() {
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
r#"
[auth]
token = "existing-token"
"#,
);
cinch_cmd(&home, "http://localhost:0")
.args(["auth", "login"])
.assert()
.success()
.stdout(predicate::str::contains("Already logged in"));
}
#[tokio::test]
async fn test_api_keys_list() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users/me/api-keys"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"api_keys": [
{
"id": "key_1",
"name": "cli",
"key_prefix": "ck_live_ab",
"created_at": 1700000000
},
{
"id": "key_2",
"name": "ci",
"key_prefix": "ck_live_cd",
"created_at": 1700100000
}
]
})))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(&home, "[auth]\ntoken = \"jwt\"\n");
cinch_cmd(&home, &mock_server.uri())
.args(["auth", "api-keys", "list"])
.assert()
.success()
.stdout(predicate::str::contains("key_1"))
.stdout(predicate::str::contains("cli"))
.stdout(predicate::str::contains("ck_live_ab"));
}
#[tokio::test]
async fn test_api_keys_list_json() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users/me/api-keys"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"api_keys": [{
"id": "key_1",
"name": "cli",
"key_prefix": "ck_live_ab",
"created_at": 1700000000
}]
})))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(&home, "[auth]\ntoken = \"jwt\"\n");
let output = cinch_cmd(&home, &mock_server.uri())
.args(["--json", "auth", "api-keys", "list"])
.output()
.expect("run");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid json");
assert_eq!(json["api_keys"][0]["id"], "key_1");
}
#[tokio::test]
async fn test_api_keys_revoke() {
let mock_server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/users/me/api-keys/key_1"))
.respond_with(ResponseTemplate::new(204))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(&home, "[auth]\ntoken = \"jwt\"\n");
cinch_cmd(&home, &mock_server.uri())
.args(["auth", "api-keys", "revoke", "key_1"])
.assert()
.success()
.stdout(predicate::str::contains("key_1 revoked"));
}
#[tokio::test]
async fn test_whoami_api_unreachable() {
let home = TempDir::new().expect("tempdir");
setup_config(&home, "[auth]\ntoken = \"jwt\"\n");
cinch_cmd(&home, "http://127.0.0.1:1")
.args(["auth", "whoami"])
.assert()
.failure()
.stderr(predicate::str::contains("error"));
}
#[test]
fn test_db_create_requires_type() {
Command::cargo_bin("cinch")
.expect("binary exists")
.args(["db", "create", "mydb"])
.assert()
.failure()
.stderr(predicate::str::contains("--type"));
}
#[test]
fn test_db_create_invalid_type() {
Command::cargo_bin("cinch")
.expect("binary exists")
.args(["db", "create", "mydb", "--type", "mongodb"])
.assert()
.failure()
.stderr(predicate::str::contains("invalid value"));
}
#[test]
fn test_db_create_valid_types_accepted() {
for db_type in ["redis", "sql", "graph"] {
let home = TempDir::new().expect("tempdir");
cinch_cmd(&home, "http://localhost:0")
.args(["db", "create", "mydb", "--type", db_type])
.assert()
.failure()
.stderr(predicate::str::contains("not yet implemented"));
}
}
#[tokio::test]
async fn test_project_cinch_file_override() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users/me"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "usr_1",
"email": "test@example.com",
})))
.mount(&mock_server)
.await;
let home = TempDir::new().expect("tempdir");
setup_config(
&home,
r#"
[auth]
token = "jwt"
[context]
org = "acme"
project = "global-proj"
environment = "global-env"
"#,
);
let workdir = TempDir::new().expect("workdir");
fs::write(
workdir.path().join(".cinch"),
"project = \"local-proj\"\nenvironment = \"staging\"\n",
)
.expect("write .cinch");
let output = cinch_cmd(&home, &mock_server.uri())
.current_dir(workdir.path())
.args(["--json", "auth", "whoami"])
.output()
.expect("run");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid json");
assert_eq!(json["context"]["project"], "local-proj");
assert_eq!(json["context"]["environment"], "staging");
assert_eq!(json["context"]["org"], "acme");
}