Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ chrono = "0.4"
dirs = "6"
libc = "0.2"
sha1_smol = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[dev-dependencies]
tempfile = "3"
29 changes: 27 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ enum Commands {
#[arg(long)]
service: Option<String>,
/// Compact mode: show only binary name instead of full path
#[arg(short, long)]
#[arg(short, long, conflicts_with = "json")]
compact: bool,
/// Output as JSON array
#[arg(long)]
json: bool,
},
/// Grant a TCC permission (inserts new entry)
Grant {
Expand Down Expand Up @@ -204,10 +207,12 @@ mod tests {
client,
service,
compact,
json,
} => {
assert_eq!(client.as_deref(), Some("apple"));
assert_eq!(service.as_deref(), Some("Camera"));
assert!(!compact);
assert!(!json);
}
_ => panic!("expected List"),
}
Expand All @@ -222,6 +227,15 @@ mod tests {
}
}

#[test]
fn parse_list_json() {
let cli = parse(&["tcc", "list", "--json"]).unwrap();
match cli.command {
Commands::List { json, .. } => assert!(json),
_ => panic!("expected List"),
}
}

#[test]
fn parse_services() {
let cli = parse(&["tcc", "services"]).unwrap();
Expand Down Expand Up @@ -401,10 +415,21 @@ fn main() {
client,
service,
compact,
json,
} => {
let db = make_db(target);
match db.list(client.as_deref(), service.as_deref()) {
Ok(entries) => print_entries(&entries, compact),
Ok(entries) => {
if json {
println!(
"{}",
serde_json::to_string_pretty(&entries)
.expect("failed to serialize entries")
);
} else {
print_entries(&entries, compact);
}
}
Err(e) => {
eprintln!("{}: {}", "Error".red().bold(), e);
process::exit(1);
Expand Down
3 changes: 2 additions & 1 deletion src/tcc.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use chrono::{Local, TimeZone};
use rusqlite::{Connection, OpenFlags};
use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -109,7 +110,7 @@ impl fmt::Display for TccError {
}
}

#[derive(Debug)]
#[derive(Debug, Serialize)]
pub struct TccEntry {
pub service_raw: String,
pub service_display: String,
Expand Down
51 changes: 51 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use serde_json::Value;
use std::process::Command;

/// Helper: run the `tccutil-rs` binary with given args, returning (stdout, stderr, success).
Expand Down Expand Up @@ -129,3 +130,53 @@ fn version_flag_prints_version() {
"version output should mention tccutil-rs"
);
}

// ── tccutil-rs list --json ──────────────────────────────────────────

#[test]
fn list_json_outputs_valid_json_array() {
let (stdout, _stderr, success) = run_tcc(&["--user", "list", "--json"]);
assert!(success, "tccutil-rs --user list --json should exit 0");

let parsed: Value = serde_json::from_str(&stdout).expect("output should be valid JSON");
assert!(parsed.is_array(), "JSON output should be an array");

// If there are entries, verify expected fields exist
if let Some(arr) = parsed.as_array() {
for entry in arr {
assert!(entry.get("service_raw").is_some(), "missing service_raw");
assert!(
entry.get("service_display").is_some(),
"missing service_display"
);
assert!(entry.get("client").is_some(), "missing client");
assert!(entry.get("auth_value").is_some(), "missing auth_value");
assert!(
entry.get("last_modified").is_some(),
"missing last_modified"
);
assert!(entry.get("is_system").is_some(), "missing is_system");
}
}
}

#[test]
fn list_json_with_client_filter_only_contains_matching_entries() {
let (stdout, _stderr, success) = run_tcc(&["--user", "list", "--json", "--client", "apple"]);
assert!(
success,
"tccutil-rs --user list --json --client apple should exit 0"
);

let parsed: Value = serde_json::from_str(&stdout).expect("output should be valid JSON");
let arr = parsed.as_array().expect("should be an array");

for entry in arr {
let client = entry["client"].as_str().expect("client should be a string");
assert!(
client.to_lowercase().contains("apple"),
"filtered entry should contain 'apple', got: {}",
client
);
}
}