use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::atlassian::client::{AtlassianClient, ConfluenceUserSearchResults};
use crate::cli::atlassian::format::{output_as, OutputFormat};
use crate::cli::atlassian::helpers::create_client;
#[derive(Parser)]
pub struct UserCommand {
#[command(subcommand)]
pub command: UserSubcommands,
}
#[derive(Subcommand)]
pub enum UserSubcommands {
Search(UserSearchCommand),
}
impl UserCommand {
pub async fn execute(self) -> Result<()> {
match self.command {
UserSubcommands::Search(cmd) => cmd.execute().await,
}
}
}
#[derive(Parser)]
pub struct UserSearchCommand {
#[arg(long)]
pub query: String,
#[arg(long, default_value_t = 25)]
pub limit: u32,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl UserSearchCommand {
pub async fn execute(self) -> Result<()> {
let (client, _instance_url) = create_client()?;
run_search(&client, &self.query, self.limit, &self.output).await
}
}
async fn run_search(
client: &AtlassianClient,
query: &str,
limit: u32,
output: &OutputFormat,
) -> Result<()> {
let result = client.search_confluence_users(query, limit).await?;
if output_as(&result, output)? {
return Ok(());
}
print_user_results(&result);
Ok(())
}
fn print_user_results(result: &ConfluenceUserSearchResults) {
if result.users.is_empty() {
println!("No users found.");
return;
}
let id_width = result
.users
.iter()
.map(|u| u.account_id.as_deref().unwrap_or("-").len())
.max()
.unwrap_or(10)
.max(10);
let name_width = result
.users
.iter()
.map(|u| u.display_name.len())
.max()
.unwrap_or(4)
.max(4);
let email_sep = "-".repeat(5);
println!(
"{:<id_width$} {:<name_width$} EMAIL",
"ACCOUNT_ID", "NAME"
);
println!(
"{:<id_width$} {:<name_width$} {email_sep}",
"-".repeat(id_width),
"-".repeat(name_width),
);
for user in &result.users {
let email = user.email.as_deref().unwrap_or("-");
let account_id = user.account_id.as_deref().unwrap_or("-");
println!(
"{:<id_width$} {:<name_width$} {email}",
account_id, user.display_name
);
}
if result.total > result.users.len() as u32 {
println!(
"\nShowing {} of {} results.",
result.users.len(),
result.total
);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::atlassian::client::ConfluenceUserSearchResult;
fn mock_client(base_url: &str) -> AtlassianClient {
AtlassianClient::new(base_url, "user@test.com", "token").unwrap()
}
fn sample_user(
account_id: Option<&str>,
display_name: &str,
email: Option<&str>,
) -> ConfluenceUserSearchResult {
ConfluenceUserSearchResult {
account_id: account_id.map(String::from),
display_name: display_name.to_string(),
email: email.map(String::from),
}
}
#[test]
fn print_results_empty() {
let result = ConfluenceUserSearchResults {
users: vec![],
total: 0,
};
print_user_results(&result);
}
#[test]
fn print_results_with_users() {
let result = ConfluenceUserSearchResults {
users: vec![
sample_user(Some("abc123"), "Alice Smith", Some("alice@example.com")),
sample_user(Some("def456"), "Bob Jones", Some("bob@example.com")),
],
total: 2,
};
print_user_results(&result);
}
#[test]
fn print_results_with_missing_email() {
let result = ConfluenceUserSearchResults {
users: vec![sample_user(Some("abc123"), "Alice Smith", None)],
total: 1,
};
print_user_results(&result);
}
#[test]
fn print_results_with_missing_account_id() {
let result = ConfluenceUserSearchResults {
users: vec![sample_user(None, "App User", None)],
total: 1,
};
print_user_results(&result);
}
#[test]
fn print_results_with_pagination() {
let result = ConfluenceUserSearchResults {
users: vec![sample_user(
Some("abc123"),
"Alice Smith",
Some("alice@example.com"),
)],
total: 50,
};
print_user_results(&result);
}
#[test]
fn user_search_command_defaults() {
let cmd = UserSearchCommand {
query: "alice".to_string(),
limit: 25,
output: OutputFormat::Table,
};
assert_eq!(cmd.query, "alice");
assert_eq!(cmd.limit, 25);
}
#[tokio::test]
async fn run_search_table_output() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/wiki/rest/api/search/user"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"results": [
{
"user": {
"accountId": "abc123",
"displayName": "Alice Smith",
"email": "alice@example.com"
}
}
]
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server.uri());
let result = run_search(&client, "alice", 25, &OutputFormat::Table).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn run_search_json_output() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/wiki/rest/api/search/user"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"results": [
{
"user": {
"accountId": "abc123",
"displayName": "Alice Smith"
}
}
]
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server.uri());
let result = run_search(&client, "alice", 25, &OutputFormat::Json).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn run_search_yaml_output() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/wiki/rest/api/search/user"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"results": []})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server.uri());
let result = run_search(&client, "nobody", 25, &OutputFormat::Yaml).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn run_search_api_error() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/wiki/rest/api/search/user"))
.respond_with(wiremock::ResponseTemplate::new(403).set_body_string("Forbidden"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server.uri());
let result = run_search(&client, "alice", 25, &OutputFormat::Table).await;
assert!(result.is_err());
}
}