feat: implement app commands
refactored to command modules with handler funcs
This commit is contained in:
1 parent
8045814b3b
commit
30a6826f46
9 files changed
+1390
-310
No files matched your search
+266
-2
@@ -1,4 +1,4 @@
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Parser)]
|
#[derive(Debug, PartialEq, Parser)]
|
||||||
#[command(name = "onx")]
|
#[command(name = "onx")]
|
||||||
@@ -28,7 +28,271 @@ pub struct Cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Subcommand)]
|
#[derive(Debug, PartialEq, Subcommand)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
#[command(about = "Check connectivity to the Onspring API")]
|
#[command(about = "Check connectivity to the Onspring API")]
|
||||||
Ping,
|
Ping,
|
||||||
|
#[command(about = "Perform operations against the apps in the instance")]
|
||||||
|
Apps {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: AppsCommand,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Subcommand)]
|
||||||
|
pub enum AppsCommand {
|
||||||
|
#[command(about = "Get information for a list of apps")]
|
||||||
|
List(PagingArgs),
|
||||||
|
|
||||||
|
#[command(about = "Get information about an app")]
|
||||||
|
Get {
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(short = 'i')]
|
||||||
|
#[arg(help = "App id to get")]
|
||||||
|
app_id: i32,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Get information for a batch of apps")]
|
||||||
|
BatchGet {
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(value_delimiter = ',')]
|
||||||
|
#[arg(short = 'i')]
|
||||||
|
#[arg(help = "Comma-separated list of app ids to get")]
|
||||||
|
ids: Vec<i32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Args, Default)]
|
||||||
|
pub struct PagingArgs {
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(short = 'n')]
|
||||||
|
#[arg(help = "The page number to retrieve")]
|
||||||
|
pub page_number: Option<i32>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(short = 's')]
|
||||||
|
#[arg(help = "The size of the page retrieve")]
|
||||||
|
pub page_size: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_cli_from<I, T>(args: I) -> crate::error::CliResult<Cli>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = T>,
|
||||||
|
T: Into<std::ffi::OsString> + Clone,
|
||||||
|
{
|
||||||
|
use clap::error::ErrorKind;
|
||||||
|
use crate::error::CliError;
|
||||||
|
|
||||||
|
Cli::try_parse_from(args).map_err(|e| match e.kind() {
|
||||||
|
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => CliError::info(e.to_string()),
|
||||||
|
_ => CliError::usage(e.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_valid_subcommand_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "ping"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_list_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "apps", "list"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Apps {
|
||||||
|
command: AppsCommand::List(PagingArgs::default()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_list_paging_args_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test",
|
||||||
|
"apps",
|
||||||
|
"list",
|
||||||
|
"--page-number",
|
||||||
|
"2",
|
||||||
|
"--page-size",
|
||||||
|
"25",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Apps {
|
||||||
|
command: AppsCommand::List(PagingArgs {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(25),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_get_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "apps", "get", "--app-id", "123"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Apps {
|
||||||
|
command: AppsCommand::Get { app_id: 123 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_get_short_flag_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "apps", "get", "-i", "123"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Apps {
|
||||||
|
command: AppsCommand::Get { app_id: 123 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_get_missing_app_id_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "apps", "get"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_batch_get_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "apps", "batch-get", "--ids", "1,2,3"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Apps {
|
||||||
|
command: AppsCommand::BatchGet {
|
||||||
|
ids: vec![1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_batch_get_without_ids_it_should_return_cli_with_empty_ids() {
|
||||||
|
let result = parse_cli_from(["test", "apps", "batch-get"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Apps {
|
||||||
|
command: AppsCommand::BatchGet { ids: vec![] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_batch_get_invalid_id_format_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "apps", "batch-get", "--ids", "invalid"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_apps_without_subcommand_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "apps"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_without_subcommand_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
assert!(err.message.contains("Usage: test [OPTIONS] <COMMAND>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_unknown_flag_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "--bogus"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.as_ref().unwrap_err().code, 2);
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_err()
|
||||||
|
.message
|
||||||
|
.contains("unexpected argument")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_help_flag_it_should_return_info_with_code_0() {
|
||||||
|
let result = parse_cli_from(["test", "--help"]);
|
||||||
|
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.code, 0);
|
||||||
|
assert!(err.message.contains(env!("CARGO_PKG_DESCRIPTION")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_version_flag_it_should_return_info_with_code_0() {
|
||||||
|
let result = parse_cli_from(["test", "--version"]);
|
||||||
|
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.code, 0);
|
||||||
|
assert!(err.message.contains(env!("CARGO_PKG_NAME")));
|
||||||
|
assert!(err.message.contains(env!("CARGO_PKG_VERSION")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
use onspring::{
|
||||||
|
App, CollectionResponse, OnspringClient, OnspringError, PagedResponse, PagingRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait OnspringRunner {
|
||||||
|
fn ping(&self) -> impl std::future::Future<Output = Result<(), OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn list_apps(
|
||||||
|
&self,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> impl std::future::Future<Output = Result<PagedResponse<App>, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn get_app(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
) -> impl std::future::Future<Output = Result<App, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn batch_get_apps(
|
||||||
|
&self,
|
||||||
|
ids: &[i32],
|
||||||
|
) -> impl std::future::Future<Output = Result<CollectionResponse<App>, OnspringError>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnspringRunner for OnspringClient {
|
||||||
|
async fn ping(&self) -> Result<(), OnspringError> {
|
||||||
|
self.ping().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_apps(
|
||||||
|
&self,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<App>, OnspringError> {
|
||||||
|
self.list_apps(paging).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_app(&self, app_id: i32) -> Result<App, OnspringError> {
|
||||||
|
self.get_app(app_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_get_apps(&self, ids: &[i32]) -> Result<CollectionResponse<App>, OnspringError> {
|
||||||
|
self.batch_get_apps(ids).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod testing {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
pub struct MockClient {
|
||||||
|
pub ping_result: Result<(), OnspringError>,
|
||||||
|
pub list_apps_result: Result<PagedResponse<App>, OnspringError>,
|
||||||
|
pub get_app_result: Result<App, OnspringError>,
|
||||||
|
pub batch_get_apps_result: Result<CollectionResponse<App>, OnspringError>,
|
||||||
|
pub list_apps_paging: Mutex<Option<Option<PagingRequest>>>,
|
||||||
|
pub get_app_id: Mutex<Option<i32>>,
|
||||||
|
pub batch_get_apps_ids: Mutex<Option<Vec<i32>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MockClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ping_result: Ok(()),
|
||||||
|
list_apps_result: Ok(PagedResponse {
|
||||||
|
page_number: None,
|
||||||
|
page_size: None,
|
||||||
|
total_pages: None,
|
||||||
|
total_records: None,
|
||||||
|
items: None,
|
||||||
|
}),
|
||||||
|
get_app_result: Ok(App {
|
||||||
|
href: None,
|
||||||
|
id: 1,
|
||||||
|
name: None,
|
||||||
|
}),
|
||||||
|
batch_get_apps_result: Ok(CollectionResponse {
|
||||||
|
count: None,
|
||||||
|
items: None,
|
||||||
|
}),
|
||||||
|
list_apps_paging: Mutex::new(None),
|
||||||
|
get_app_id: Mutex::new(None),
|
||||||
|
batch_get_apps_ids: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_onspring_error(err: &OnspringError) -> OnspringError {
|
||||||
|
match err {
|
||||||
|
OnspringError::InvalidArgument(msg) => OnspringError::InvalidArgument(msg.clone()),
|
||||||
|
OnspringError::Api {
|
||||||
|
status_code,
|
||||||
|
message,
|
||||||
|
} => OnspringError::Api {
|
||||||
|
status_code: *status_code,
|
||||||
|
message: message.clone(),
|
||||||
|
},
|
||||||
|
OnspringError::Serialization(_) => {
|
||||||
|
let serde_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
|
||||||
|
OnspringError::Serialization(serde_err)
|
||||||
|
}
|
||||||
|
OnspringError::Http(_) => OnspringError::InvalidArgument("http error".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnspringRunner for MockClient {
|
||||||
|
async fn ping(&self) -> Result<(), OnspringError> {
|
||||||
|
match &self.ping_result {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_apps(
|
||||||
|
&self,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<App>, OnspringError> {
|
||||||
|
*self.list_apps_paging.lock().unwrap() = Some(paging);
|
||||||
|
match &self.list_apps_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_app(&self, app_id: i32) -> Result<App, OnspringError> {
|
||||||
|
*self.get_app_id.lock().unwrap() = Some(app_id);
|
||||||
|
match &self.get_app_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_get_apps(&self, ids: &[i32]) -> Result<CollectionResponse<App>, OnspringError> {
|
||||||
|
*self.batch_get_apps_ids.lock().unwrap() = Some(ids.to_vec());
|
||||||
|
match &self.batch_get_apps_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::cli::AppsCommand;
|
||||||
|
use crate::client::OnspringRunner;
|
||||||
|
use crate::error::CliResult;
|
||||||
|
use crate::output::{AppOutput, CollectionOutput, PagedOutput, write_json};
|
||||||
|
use crate::validation::{paging_request, validate_ids, validate_positive_i32};
|
||||||
|
|
||||||
|
pub async fn handle<C: OnspringRunner, W: Write>(
|
||||||
|
command: &AppsCommand,
|
||||||
|
client: &C,
|
||||||
|
writer: &mut W,
|
||||||
|
pretty: bool,
|
||||||
|
) -> CliResult<()> {
|
||||||
|
match command {
|
||||||
|
AppsCommand::List(paging_args) => {
|
||||||
|
let paging = paging_request(paging_args)?;
|
||||||
|
let response = client.list_apps(paging).await?;
|
||||||
|
let output: PagedOutput<AppOutput> = response.into();
|
||||||
|
write_json(writer, &output, pretty)?;
|
||||||
|
}
|
||||||
|
AppsCommand::Get { app_id } => {
|
||||||
|
validate_positive_i32(*app_id, "app_id")?;
|
||||||
|
let response = client.get_app(*app_id).await?;
|
||||||
|
let output: AppOutput = response.into();
|
||||||
|
write_json(writer, &output, pretty)?;
|
||||||
|
}
|
||||||
|
AppsCommand::BatchGet { ids } => {
|
||||||
|
validate_ids(ids, "ids", Some(100))?;
|
||||||
|
let response = client.batch_get_apps(ids).await?;
|
||||||
|
let output: CollectionOutput<AppOutput> = response.into();
|
||||||
|
write_json(writer, &output, pretty)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use onspring::{App, CollectionResponse, OnspringError, PagedResponse};
|
||||||
|
|
||||||
|
use crate::cli::PagingArgs;
|
||||||
|
use crate::client::testing::MockClient;
|
||||||
|
use crate::error::CliError;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_list_succeeds_it_should_write_paged_output() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_apps_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(50),
|
||||||
|
total_pages: Some(1),
|
||||||
|
total_records: Some(1),
|
||||||
|
items: Some(vec![App {
|
||||||
|
href: Some("https://api.onspring.com/apps/1".to_string()),
|
||||||
|
id: 1,
|
||||||
|
name: Some("Test App".to_string()),
|
||||||
|
}]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::List(PagingArgs::default());
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
written.trim(),
|
||||||
|
r#"{"pageNumber":1,"pageSize":50,"totalPages":1,"totalRecords":1,"items":[{"href":"https://api.onspring.com/apps/1","id":1,"name":"Test App"}]}"#
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
mock_client
|
||||||
|
.list_apps_paging
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_list_with_paging_args_it_should_pass_paging_request() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_apps_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(10),
|
||||||
|
total_pages: Some(5),
|
||||||
|
total_records: Some(50),
|
||||||
|
items: Some(vec![]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::List(PagingArgs {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(10),
|
||||||
|
});
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let paging = mock_client
|
||||||
|
.list_apps_paging
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.take()
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(paging.page_number, 2);
|
||||||
|
assert_eq!(paging.page_size, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_list_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_apps_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(50),
|
||||||
|
total_pages: Some(1),
|
||||||
|
total_records: Some(1),
|
||||||
|
items: Some(vec![App {
|
||||||
|
href: Some("https://api.onspring.com/apps/1".to_string()),
|
||||||
|
id: 1,
|
||||||
|
name: Some("Test App".to_string()),
|
||||||
|
}]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::List(PagingArgs::default());
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, true).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
let expected = concat!(
|
||||||
|
"{\n",
|
||||||
|
" \"pageNumber\": 1,\n",
|
||||||
|
" \"pageSize\": 50,\n",
|
||||||
|
" \"totalPages\": 1,\n",
|
||||||
|
" \"totalRecords\": 1,\n",
|
||||||
|
" \"items\": [\n",
|
||||||
|
" {\n",
|
||||||
|
" \"href\": \"https://api.onspring.com/apps/1\",\n",
|
||||||
|
" \"id\": 1,\n",
|
||||||
|
" \"name\": \"Test App\"\n",
|
||||||
|
" }\n",
|
||||||
|
" ]\n",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_list_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_apps_result: Err(OnspringError::Api {
|
||||||
|
status_code: 401,
|
||||||
|
message: "Unauthorized".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::List(PagingArgs::default());
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::runtime("API request failed (401): Unauthorized"))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_list_has_invalid_paging_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = AppsCommand::List(PagingArgs {
|
||||||
|
page_number: Some(0),
|
||||||
|
page_size: None,
|
||||||
|
});
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("page_number must be greater than 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert!(mock_client.list_apps_paging.lock().unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_get_succeeds_it_should_write_app_output() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_app_result: Ok(App {
|
||||||
|
href: Some("https://api.onspring.com/apps/123".to_string()),
|
||||||
|
id: 123,
|
||||||
|
name: Some("Test App".to_string()),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::Get { app_id: 123 };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
written.trim(),
|
||||||
|
r#"{"href":"https://api.onspring.com/apps/123","id":123,"name":"Test App"}"#
|
||||||
|
);
|
||||||
|
assert_eq!(*mock_client.get_app_id.lock().unwrap(), Some(123));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_get_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_app_result: Ok(App {
|
||||||
|
href: Some("https://api.onspring.com/apps/123".to_string()),
|
||||||
|
id: 123,
|
||||||
|
name: Some("Test App".to_string()),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::Get { app_id: 123 };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, true).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
let expected = concat!(
|
||||||
|
"{\n",
|
||||||
|
" \"href\": \"https://api.onspring.com/apps/123\",\n",
|
||||||
|
" \"id\": 123,\n",
|
||||||
|
" \"name\": \"Test App\"\n",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_get_with_invalid_id_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = AppsCommand::Get { app_id: 0 };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("app_id must be greater than 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert_eq!(*mock_client.get_app_id.lock().unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_get_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_app_result: Err(OnspringError::Api {
|
||||||
|
status_code: 404,
|
||||||
|
message: "App not found".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::Get { app_id: 123 };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::runtime("API request failed (404): App not found"))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_batch_get_succeeds_it_should_write_collection_output() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
batch_get_apps_result: Ok(CollectionResponse {
|
||||||
|
count: Some(2),
|
||||||
|
items: Some(vec![
|
||||||
|
App {
|
||||||
|
href: Some("https://api.onspring.com/apps/1".to_string()),
|
||||||
|
id: 1,
|
||||||
|
name: Some("App 1".to_string()),
|
||||||
|
},
|
||||||
|
App {
|
||||||
|
href: Some("https://api.onspring.com/apps/2".to_string()),
|
||||||
|
id: 2,
|
||||||
|
name: Some("App 2".to_string()),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::BatchGet { ids: vec![1, 2] };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
written.trim(),
|
||||||
|
r#"{"count":2,"items":[{"href":"https://api.onspring.com/apps/1","id":1,"name":"App 1"},{"href":"https://api.onspring.com/apps/2","id":2,"name":"App 2"}]}"#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*mock_client.batch_get_apps_ids.lock().unwrap(),
|
||||||
|
Some(vec![1, 2])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_batch_get_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
batch_get_apps_result: Ok(CollectionResponse {
|
||||||
|
count: Some(1),
|
||||||
|
items: Some(vec![App {
|
||||||
|
href: Some("https://api.onspring.com/apps/1".to_string()),
|
||||||
|
id: 1,
|
||||||
|
name: Some("App 1".to_string()),
|
||||||
|
}]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::BatchGet { ids: vec![1] };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, true).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
let expected = concat!(
|
||||||
|
"{\n",
|
||||||
|
" \"count\": 1,\n",
|
||||||
|
" \"items\": [\n",
|
||||||
|
" {\n",
|
||||||
|
" \"href\": \"https://api.onspring.com/apps/1\",\n",
|
||||||
|
" \"id\": 1,\n",
|
||||||
|
" \"name\": \"App 1\"\n",
|
||||||
|
" }\n",
|
||||||
|
" ]\n",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_batch_get_with_empty_ids_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = AppsCommand::BatchGet { ids: vec![] };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Err(CliError::usage("ids cannot be empty.")));
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert_eq!(*mock_client.batch_get_apps_ids.lock().unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_batch_get_with_invalid_ids_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = AppsCommand::BatchGet {
|
||||||
|
ids: vec![1, 0, 2],
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("ids must only contain values > 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert_eq!(*mock_client.batch_get_apps_ids.lock().unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_batch_get_with_too_many_ids_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = AppsCommand::BatchGet {
|
||||||
|
ids: vec![1; 101],
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage(
|
||||||
|
"ids cannot contain more than 100 values."
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert_eq!(*mock_client.batch_get_apps_ids.lock().unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_apps_batch_get_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
batch_get_apps_result: Err(OnspringError::Api {
|
||||||
|
status_code: 500,
|
||||||
|
message: "".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = AppsCommand::BatchGet { ids: vec![1, 2] };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::runtime("API request failed with status 500."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
pub mod apps;
|
||||||
|
pub mod ping;
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::cli::{Cli, Command};
|
||||||
|
use crate::client::OnspringRunner;
|
||||||
|
use crate::error::CliResult;
|
||||||
|
|
||||||
|
pub async fn handle<C: OnspringRunner, W: Write>(
|
||||||
|
cli: &Cli,
|
||||||
|
client: &C,
|
||||||
|
writer: &mut W,
|
||||||
|
) -> CliResult<()> {
|
||||||
|
match &cli.command {
|
||||||
|
Command::Ping => ping::handle(client, writer, cli.pretty).await,
|
||||||
|
Command::Apps { command } => apps::handle(command, client, writer, cli.pretty).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::client::OnspringRunner;
|
||||||
|
use crate::error::CliResult;
|
||||||
|
use crate::output::{SuccessResponse, write_json};
|
||||||
|
|
||||||
|
pub async fn handle<C: OnspringRunner, W: Write>(
|
||||||
|
client: &C,
|
||||||
|
writer: &mut W,
|
||||||
|
pretty: bool,
|
||||||
|
) -> CliResult<()> {
|
||||||
|
client.ping().await?;
|
||||||
|
write_json(writer, &SuccessResponse { ok: true }, pretty)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use onspring::OnspringError;
|
||||||
|
|
||||||
|
use crate::client::testing::MockClient;
|
||||||
|
use crate::error::CliError;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_ping_succeeds_it_should_write_success_response() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
ping_result: Ok(()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), r#"{"ok":true}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_ping_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
ping_result: Ok(()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&mock_client, &mut buffer, true).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), "{\n \"ok\": true\n}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_ping_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
ping_result: Err(OnspringError::InvalidArgument("Invalid Key".to_string())),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Err(CliError::usage("Invalid request: Invalid Key")));
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
use onspring::OnspringError;
|
||||||
|
|
||||||
|
use crate::output::render_error;
|
||||||
|
|
||||||
|
pub type CliResult<T> = Result<T, CliError>;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct CliError {
|
||||||
|
pub code: i32,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliError {
|
||||||
|
pub fn info(message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
code: 0,
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runtime(message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
code: 1,
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn usage(message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
code: 2,
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OnspringError> for CliError {
|
||||||
|
fn from(value: OnspringError) -> Self {
|
||||||
|
match value {
|
||||||
|
OnspringError::InvalidArgument(message) => Self::usage(format!("Invalid request: {message}")),
|
||||||
|
OnspringError::Api {
|
||||||
|
status_code,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
if message.is_empty() {
|
||||||
|
Self::runtime(format!("API request failed with status {status_code}."))
|
||||||
|
} else {
|
||||||
|
Self::runtime(format!("API request failed ({status_code}): {message}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnspringError::Http(_) => Self::runtime("Network request failed."),
|
||||||
|
OnspringError::Serialization(_) => Self::runtime("Failed to parse API response."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_cli_error(err: &CliError, pretty: bool) -> String {
|
||||||
|
render_error(err, pretty)
|
||||||
|
.unwrap_or_else(|_| "{\"error\":{\"code\":1,\"message\":\"Unexpected error.\"}}".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_cli_error_when_called_with_error_it_should_render_json() {
|
||||||
|
let error = CliError {
|
||||||
|
code: 1,
|
||||||
|
message: String::from("Something went wrong"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = render_cli_error(&error, false);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
r#"{"error":{"code":1,"message":"Something went wrong"}}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_constructors_should_set_expected_codes_and_messages() {
|
||||||
|
let info = CliError::info("info message");
|
||||||
|
assert_eq!(info.code, 0);
|
||||||
|
assert_eq!(info.message, "info message");
|
||||||
|
|
||||||
|
let runtime = CliError::runtime("runtime message");
|
||||||
|
assert_eq!(runtime.code, 1);
|
||||||
|
assert_eq!(runtime.message, "runtime message");
|
||||||
|
|
||||||
|
let usage = CliError::usage("usage message");
|
||||||
|
assert_eq!(usage.code, 2);
|
||||||
|
assert_eq!(usage.message, "usage message");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_from_onspring_error_invalid_argument() {
|
||||||
|
let err = OnspringError::InvalidArgument("bad param".to_string());
|
||||||
|
let cli_err = CliError::from(err);
|
||||||
|
|
||||||
|
assert_eq!(cli_err.code, 2);
|
||||||
|
assert_eq!(cli_err.message, "Invalid request: bad param");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_from_onspring_error_api_with_message() {
|
||||||
|
let err = OnspringError::Api {
|
||||||
|
status_code: 404,
|
||||||
|
message: "Resource not found".to_string(),
|
||||||
|
};
|
||||||
|
let cli_err = CliError::from(err);
|
||||||
|
|
||||||
|
assert_eq!(cli_err.code, 1);
|
||||||
|
assert_eq!(
|
||||||
|
cli_err.message,
|
||||||
|
"API request failed (404): Resource not found"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_from_onspring_error_api_with_empty_message() {
|
||||||
|
let err = OnspringError::Api {
|
||||||
|
status_code: 500,
|
||||||
|
message: String::new(),
|
||||||
|
};
|
||||||
|
let cli_err = CliError::from(err);
|
||||||
|
|
||||||
|
assert_eq!(cli_err.code, 1);
|
||||||
|
assert_eq!(cli_err.message, "API request failed with status 500.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_from_onspring_error_serialization() {
|
||||||
|
let serde_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
|
||||||
|
let err = OnspringError::Serialization(serde_err);
|
||||||
|
let cli_err = CliError::from(err);
|
||||||
|
|
||||||
|
assert_eq!(cli_err.code, 1);
|
||||||
|
assert_eq!(cli_err.message, "Failed to parse API response.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-308
@@ -1,72 +1,15 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
|
mod client;
|
||||||
|
mod commands;
|
||||||
|
mod error;
|
||||||
mod output;
|
mod output;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
use clap::{Parser, error::ErrorKind};
|
use onspring::OnspringClient;
|
||||||
use cli::{Cli, Command};
|
|
||||||
|
|
||||||
use onspring::{OnspringClient, OnspringError};
|
pub use cli::{Cli, Command, parse_cli_from};
|
||||||
use output::{SuccessResponse, render_error, write_json};
|
pub use client::OnspringRunner;
|
||||||
|
pub use error::{CliError, CliResult, render_cli_error};
|
||||||
pub type CliResult<T> = Result<T, CliError>;
|
|
||||||
|
|
||||||
pub trait OnspringRunner {
|
|
||||||
fn ping(&self) -> impl std::future::Future<Output = Result<(), OnspringError>> + Send;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OnspringRunner for OnspringClient {
|
|
||||||
async fn ping(&self) -> Result<(), OnspringError> {
|
|
||||||
self.ping().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub struct CliError {
|
|
||||||
pub code: i32,
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CliError {
|
|
||||||
pub fn info(message: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
code: 0,
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runtime(message: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
code: 1,
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn usage(message: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
code: 2,
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<OnspringError> for CliError {
|
|
||||||
fn from(value: OnspringError) -> Self {
|
|
||||||
match value {
|
|
||||||
OnspringError::InvalidArgument(message) => Self::usage(format!("Invalid request: {message}")),
|
|
||||||
OnspringError::Api {
|
|
||||||
status_code,
|
|
||||||
message,
|
|
||||||
} => {
|
|
||||||
if message.is_empty() {
|
|
||||||
Self::runtime(format!("API request failed with status {status_code}."))
|
|
||||||
} else {
|
|
||||||
Self::runtime(format!("API request failed ({status_code}): {message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OnspringError::Http(_) => Self::runtime("Network request failed."),
|
|
||||||
OnspringError::Serialization(_) => Self::runtime("Failed to parse API response."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run(cli: Cli) -> CliResult<()> {
|
pub async fn run(cli: Cli) -> CliResult<()> {
|
||||||
let api_key = cli
|
let api_key = cli
|
||||||
@@ -94,202 +37,13 @@ pub async fn run_with_client<C: OnspringRunner, W: std::io::Write>(
|
|||||||
client: &C,
|
client: &C,
|
||||||
writer: &mut W,
|
writer: &mut W,
|
||||||
) -> CliResult<()> {
|
) -> CliResult<()> {
|
||||||
match cli.command {
|
commands::handle(cli, client, writer).await
|
||||||
Command::Ping => {
|
|
||||||
client.ping().await?;
|
|
||||||
write_json(writer, &SuccessResponse { ok: true }, cli.pretty)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_cli_from<I, T>(args: I) -> CliResult<Cli>
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = T>,
|
|
||||||
T: Into<std::ffi::OsString> + Clone,
|
|
||||||
{
|
|
||||||
Cli::try_parse_from(args).map_err(|e| match e.kind() {
|
|
||||||
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => CliError::info(e.to_string()),
|
|
||||||
_ => CliError::usage(e.to_string()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_cli_error(err: &CliError, pretty: bool) -> String {
|
|
||||||
render_error(err, pretty)
|
|
||||||
.unwrap_or_else(|_| "{\"error\":{\"code\":1,\"message\":\"Unexpected error.\"}}".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
struct MockClient {
|
|
||||||
ping_result: Result<(), OnspringError>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OnspringRunner for MockClient {
|
|
||||||
async fn ping(&self) -> Result<(), OnspringError> {
|
|
||||||
match &self.ping_result {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(OnspringError::InvalidArgument(msg)) => {
|
|
||||||
Err(OnspringError::InvalidArgument(msg.clone()))
|
|
||||||
}
|
|
||||||
Err(OnspringError::Api {
|
|
||||||
status_code,
|
|
||||||
message,
|
|
||||||
}) => Err(OnspringError::Api {
|
|
||||||
status_code: *status_code,
|
|
||||||
message: message.clone(),
|
|
||||||
}),
|
|
||||||
Err(OnspringError::Serialization(_)) => {
|
|
||||||
let serde_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
|
|
||||||
Err(OnspringError::Serialization(serde_err))
|
|
||||||
}
|
|
||||||
Err(OnspringError::Http(_)) => {
|
|
||||||
Err(OnspringError::InvalidArgument("http error".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_cli_from_when_called_with_valid_subcommand_it_should_return_cli() {
|
|
||||||
let result = parse_cli_from(["test", "ping"]);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
Ok(Cli {
|
|
||||||
api_key: None,
|
|
||||||
base_url: None,
|
|
||||||
pretty: false,
|
|
||||||
command: Command::Ping,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_cli_from_when_called_without_subcommand_it_should_return_usage_error() {
|
|
||||||
let result = parse_cli_from(["test"]);
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
let err = result.unwrap_err();
|
|
||||||
assert_eq!(err.code, 2);
|
|
||||||
assert!(err.message.contains("Usage: test [OPTIONS] <COMMAND>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_cli_from_when_called_with_unknown_flag_it_should_return_usage_error() {
|
|
||||||
let result = parse_cli_from(["test", "--bogus"]);
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert_eq!(result.as_ref().unwrap_err().code, 2);
|
|
||||||
assert!(
|
|
||||||
result
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_err()
|
|
||||||
.message
|
|
||||||
.contains("unexpected argument")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_cli_from_when_called_with_help_flag_it_should_return_info_with_code_0() {
|
|
||||||
let result = parse_cli_from(["test", "--help"]);
|
|
||||||
|
|
||||||
let err = result.unwrap_err();
|
|
||||||
|
|
||||||
assert_eq!(err.code, 0);
|
|
||||||
assert!(err.message.contains(env!("CARGO_PKG_DESCRIPTION")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_cli_from_when_called_with_version_flag_it_should_return_info_with_code_0() {
|
|
||||||
let result = parse_cli_from(["test", "--version"]);
|
|
||||||
|
|
||||||
let err = result.unwrap_err();
|
|
||||||
|
|
||||||
assert_eq!(err.code, 0);
|
|
||||||
assert!(err.message.contains(env!("CARGO_PKG_NAME")));
|
|
||||||
assert!(err.message.contains(env!("CARGO_PKG_VERSION")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_cli_error_when_called_with_error_it_should_render_json() {
|
|
||||||
let error = CliError {
|
|
||||||
code: 1,
|
|
||||||
message: String::from("Something went wrong"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = render_cli_error(&error, false);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
r#"{"error":{"code":1,"message":"Something went wrong"}}"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_error_constructors_should_set_expected_codes_and_messages() {
|
|
||||||
let info = CliError::info("info message");
|
|
||||||
assert_eq!(info.code, 0);
|
|
||||||
assert_eq!(info.message, "info message");
|
|
||||||
|
|
||||||
let runtime = CliError::runtime("runtime message");
|
|
||||||
assert_eq!(runtime.code, 1);
|
|
||||||
assert_eq!(runtime.message, "runtime message");
|
|
||||||
|
|
||||||
let usage = CliError::usage("usage message");
|
|
||||||
assert_eq!(usage.code, 2);
|
|
||||||
assert_eq!(usage.message, "usage message");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_error_from_onspring_error_invalid_argument() {
|
|
||||||
let err = OnspringError::InvalidArgument("bad param".to_string());
|
|
||||||
let cli_err = CliError::from(err);
|
|
||||||
|
|
||||||
assert_eq!(cli_err.code, 2);
|
|
||||||
assert_eq!(cli_err.message, "Invalid request: bad param");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_error_from_onspring_error_api_with_message() {
|
|
||||||
let err = OnspringError::Api {
|
|
||||||
status_code: 404,
|
|
||||||
message: "Resource not found".to_string(),
|
|
||||||
};
|
|
||||||
let cli_err = CliError::from(err);
|
|
||||||
|
|
||||||
assert_eq!(cli_err.code, 1);
|
|
||||||
assert_eq!(
|
|
||||||
cli_err.message,
|
|
||||||
"API request failed (404): Resource not found"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_error_from_onspring_error_api_with_empty_message() {
|
|
||||||
let err = OnspringError::Api {
|
|
||||||
status_code: 500,
|
|
||||||
message: String::new(),
|
|
||||||
};
|
|
||||||
let cli_err = CliError::from(err);
|
|
||||||
|
|
||||||
assert_eq!(cli_err.code, 1);
|
|
||||||
assert_eq!(cli_err.message, "API request failed with status 500.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_error_from_onspring_error_serialization() {
|
|
||||||
let serde_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
|
|
||||||
let err = OnspringError::Serialization(serde_err);
|
|
||||||
let cli_err = CliError::from(err);
|
|
||||||
|
|
||||||
assert_eq!(cli_err.code, 1);
|
|
||||||
assert_eq!(cli_err.message, "Failed to parse API response.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn run_when_missing_api_key_it_should_return_usage_error() {
|
async fn run_when_missing_api_key_it_should_return_usage_error() {
|
||||||
let cli = Cli {
|
let cli = Cli {
|
||||||
@@ -324,71 +78,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn run_with_client_when_ping_succeeds_it_should_write_success_response() {
|
async fn run_with_client_it_should_delegate_to_commands() {
|
||||||
let mock_client = MockClient {
|
let mock_client = client::testing::MockClient::default();
|
||||||
ping_result: Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let cli = Cli {
|
let cli = Cli {
|
||||||
api_key: Some("key".to_string()),
|
api_key: Some("key".to_string()),
|
||||||
base_url: None,
|
base_url: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Ping,
|
command: Command::Ping,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
let result = run_with_client(&cli, &mock_client, &mut buffer).await;
|
let result = run_with_client(&cli, &mock_client, &mut buffer).await;
|
||||||
|
|
||||||
assert_eq!(result, Ok(()));
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
let written = String::from_utf8(buffer).unwrap();
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
assert_eq!(written.trim(), r#"{"ok":true}"#);
|
assert_eq!(written.trim(), r#"{"ok":true}"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn run_with_client_when_ping_succeeds_pretty_it_should_write_pretty_json() {
|
|
||||||
let mock_client = MockClient {
|
|
||||||
ping_result: Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let cli = Cli {
|
|
||||||
api_key: Some("key".to_string()),
|
|
||||||
base_url: None,
|
|
||||||
pretty: true,
|
|
||||||
command: Command::Ping,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
|
|
||||||
let result = run_with_client(&cli, &mock_client, &mut buffer).await;
|
|
||||||
|
|
||||||
assert_eq!(result, Ok(()));
|
|
||||||
|
|
||||||
let written = String::from_utf8(buffer).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(written.trim(), "{\n \"ok\": true\n}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn run_with_client_when_ping_fails_it_should_return_mapped_cli_error() {
|
|
||||||
let mock_client = MockClient {
|
|
||||||
ping_result: Err(OnspringError::InvalidArgument("Invalid Key".to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
let cli = Cli {
|
|
||||||
api_key: Some("key".to_string()),
|
|
||||||
base_url: None,
|
|
||||||
pretty: false,
|
|
||||||
command: Command::Ping,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
|
|
||||||
let result = run_with_client(&cli, &mock_client, &mut buffer).await;
|
|
||||||
|
|
||||||
assert_eq!(result, Err(CliError::usage("Invalid request: Invalid Key")));
|
|
||||||
assert!(buffer.is_empty());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
use onspring::{App, CollectionResponse, PagedResponse};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{CliError, CliResult};
|
use crate::{CliError, CliResult};
|
||||||
@@ -21,6 +22,72 @@ pub struct SuccessResponse {
|
|||||||
pub ok: bool,
|
pub ok: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PagedOutput<T> {
|
||||||
|
pub page_number: Option<i32>,
|
||||||
|
pub page_size: Option<i32>,
|
||||||
|
pub total_pages: Option<i32>,
|
||||||
|
pub total_records: Option<i32>,
|
||||||
|
pub items: Option<Vec<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U> From<PagedResponse<U>> for PagedOutput<T>
|
||||||
|
where
|
||||||
|
T: From<U>,
|
||||||
|
{
|
||||||
|
fn from(value: PagedResponse<U>) -> Self {
|
||||||
|
Self {
|
||||||
|
page_number: value.page_number,
|
||||||
|
page_size: value.page_size,
|
||||||
|
total_pages: value.total_pages,
|
||||||
|
total_records: value.total_records,
|
||||||
|
items: value
|
||||||
|
.items
|
||||||
|
.map(|items| items.into_iter().map(T::from).collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CollectionOutput<T> {
|
||||||
|
pub count: Option<i32>,
|
||||||
|
pub items: Option<Vec<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U> From<CollectionResponse<U>> for CollectionOutput<T>
|
||||||
|
where
|
||||||
|
T: From<U>,
|
||||||
|
{
|
||||||
|
fn from(value: CollectionResponse<U>) -> Self {
|
||||||
|
Self {
|
||||||
|
count: value.count,
|
||||||
|
items: value
|
||||||
|
.items
|
||||||
|
.map(|items| items.into_iter().map(T::from).collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppOutput {
|
||||||
|
pub href: Option<String>,
|
||||||
|
pub id: i32,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<App> for AppOutput {
|
||||||
|
fn from(value: App) -> Self {
|
||||||
|
Self {
|
||||||
|
href: value.href,
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_json<T: Serialize>(value: &T, pretty: bool) -> CliResult<String> {
|
pub fn render_json<T: Serialize>(value: &T, pretty: bool) -> CliResult<String> {
|
||||||
if pretty {
|
if pretty {
|
||||||
serde_json::to_string_pretty(value).map_err(|_| CliError::runtime("Failed to serialize output"))
|
serde_json::to_string_pretty(value).map_err(|_| CliError::runtime("Failed to serialize output"))
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
use onspring::PagingRequest;
|
||||||
|
|
||||||
|
use crate::cli::PagingArgs;
|
||||||
|
use crate::error::{CliError, CliResult};
|
||||||
|
|
||||||
|
pub fn paging_request(args: &PagingArgs) -> CliResult<Option<PagingRequest>> {
|
||||||
|
if args.page_number.is_none() && args.page_size.is_none() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let page_number = args.page_number.unwrap_or(1);
|
||||||
|
let page_size = args.page_size.unwrap_or(50);
|
||||||
|
|
||||||
|
if page_number <= 0 {
|
||||||
|
return Err(CliError::usage("page_number must be greater than 0."));
|
||||||
|
}
|
||||||
|
if page_size <= 0 {
|
||||||
|
return Err(CliError::usage("page_size must be greater than 0."));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(PagingRequest {
|
||||||
|
page_number,
|
||||||
|
page_size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_positive_i32(value: i32, field: &str) -> CliResult<()> {
|
||||||
|
if value <= 0 {
|
||||||
|
return Err(CliError::usage(format!("{field} must be greater than 0.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_ids(ids: &[i32], name: &str, max_len: Option<usize>) -> CliResult<()> {
|
||||||
|
if ids.is_empty() {
|
||||||
|
return Err(CliError::usage(format!("{name} cannot be empty.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(max_len) = max_len
|
||||||
|
&& ids.len() > max_len
|
||||||
|
{
|
||||||
|
return Err(CliError::usage(format!(
|
||||||
|
"{name} cannot contain more than {max_len} values."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ids.iter().any(|id| *id <= 0) {
|
||||||
|
return Err(CliError::usage(format!(
|
||||||
|
"{name} must only contain values > 0."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paging_request_when_no_args_it_should_return_none() {
|
||||||
|
let args = PagingArgs {
|
||||||
|
page_number: None,
|
||||||
|
page_size: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = paging_request(&args);
|
||||||
|
|
||||||
|
assert!(matches!(result, Ok(None)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paging_request_when_page_number_provided_it_should_default_page_size() {
|
||||||
|
let args = PagingArgs {
|
||||||
|
page_number: Some(3),
|
||||||
|
page_size: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = paging_request(&args).unwrap();
|
||||||
|
let paging = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(paging.page_number, 3);
|
||||||
|
assert_eq!(paging.page_size, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paging_request_when_page_size_provided_it_should_default_page_number() {
|
||||||
|
let args = PagingArgs {
|
||||||
|
page_number: None,
|
||||||
|
page_size: Some(25),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = paging_request(&args).unwrap();
|
||||||
|
let paging = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(paging.page_number, 1);
|
||||||
|
assert_eq!(paging.page_size, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paging_request_when_both_args_provided_it_should_return_paging_request() {
|
||||||
|
let args = PagingArgs {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(10),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = paging_request(&args).unwrap();
|
||||||
|
let paging = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(paging.page_number, 2);
|
||||||
|
assert_eq!(paging.page_size, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paging_request_when_page_number_zero_or_negative_it_should_return_usage_error() {
|
||||||
|
let args_zero = PagingArgs {
|
||||||
|
page_number: Some(0),
|
||||||
|
page_size: Some(10),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
paging_request(&args_zero).unwrap_err(),
|
||||||
|
CliError::usage("page_number must be greater than 0.")
|
||||||
|
);
|
||||||
|
|
||||||
|
let args_neg = PagingArgs {
|
||||||
|
page_number: Some(-1),
|
||||||
|
page_size: Some(10),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
paging_request(&args_neg).unwrap_err(),
|
||||||
|
CliError::usage("page_number must be greater than 0.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paging_request_when_page_size_zero_or_negative_it_should_return_usage_error() {
|
||||||
|
let args_zero = PagingArgs {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(0),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
paging_request(&args_zero).unwrap_err(),
|
||||||
|
CliError::usage("page_size must be greater than 0.")
|
||||||
|
);
|
||||||
|
|
||||||
|
let args_neg = PagingArgs {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(-5),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
paging_request(&args_neg).unwrap_err(),
|
||||||
|
CliError::usage("page_size must be greater than 0.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_positive_i32_when_valid_it_should_return_ok() {
|
||||||
|
assert_eq!(validate_positive_i32(1, "app_id"), Ok(()));
|
||||||
|
assert_eq!(validate_positive_i32(100, "app_id"), Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_positive_i32_when_zero_or_negative_it_should_return_usage_error() {
|
||||||
|
assert_eq!(
|
||||||
|
validate_positive_i32(0, "app_id"),
|
||||||
|
Err(CliError::usage("app_id must be greater than 0."))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
validate_positive_i32(-1, "app_id"),
|
||||||
|
Err(CliError::usage("app_id must be greater than 0."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_ids_when_valid_it_should_return_ok() {
|
||||||
|
assert_eq!(validate_ids(&[1, 2, 3], "ids", Some(100)), Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_ids_when_empty_it_should_return_usage_error() {
|
||||||
|
assert_eq!(
|
||||||
|
validate_ids(&[], "ids", Some(100)),
|
||||||
|
Err(CliError::usage("ids cannot be empty."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_ids_when_contains_zero_or_negative_it_should_return_usage_error() {
|
||||||
|
assert_eq!(
|
||||||
|
validate_ids(&[1, 0, 3], "ids", Some(100)),
|
||||||
|
Err(CliError::usage("ids must only contain values > 0."))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
validate_ids(&[1, -2, 3], "ids", Some(100)),
|
||||||
|
Err(CliError::usage("ids must only contain values > 0."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_ids_when_exceeds_max_len_it_should_return_usage_error() {
|
||||||
|
let ids = vec![1; 101];
|
||||||
|
assert_eq!(
|
||||||
|
validate_ids(&ids, "ids", Some(100)),
|
||||||
|
Err(CliError::usage(
|
||||||
|
"ids cannot contain more than 100 values."
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user