diff --git a/src/api.rs b/src/api.rs index 50f0c7e..150add9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -160,3 +160,45 @@ pub fn parse_response_status(response: &reqwest::Response) -> Result<(), ApiErro )), } } + +#[cfg(test)] +mod tests { + use http::header::HeaderValue; + + use super::*; + + /// Test parsing a valid RFC5988 header value. + /// + /// Attempt to parse a valid RFC5988 header value, and ensure that the + /// parsed URL was returned as expected. + #[async_std::test] + async fn test_parse_rfc5988_valid() { + // Mock a valid RFC5988 header value + let valid_header_value = + HeaderValue::from_str(r#"; rel="related""#) + .expect("Failed to create valid header value"); + + // Call the parse_rfc5988 function with the valid header value + let result = parse_rfc5988(Some(&valid_header_value)).unwrap(); + + // Assert that the function returned the expected URL as Some(String) + assert_eq!(result, Some(String::from("https://example.com/related"))); + } + + /// Test parsing an invalid RFC5988 header value. + /// + /// Attempt to parse an invalid string as RFC5988, ensuring that the `None` + /// variant is returned. + #[async_std::test] + async fn test_parse_rfc5988_invalid() { + // Mock a valid RFC5988 header value + let invalid_header_value = HeaderValue::from_str(r#"invalid header value"#) + .expect("Failed to create valid header value"); + + // Call the parse_rfc5988 function with the valid header value + let result = parse_rfc5988(Some(&invalid_header_value)).unwrap(); + + // Assert that the function returned the expected URL as Some(String) + assert_eq!(result, None); + } +} diff --git a/src/cli.rs b/src/cli.rs index a188ddc..1be9423 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -26,7 +26,7 @@ use clap::ValueEnum; /// Dredge is a command line tool for working with the Docker Registry /// V2 API. -#[derive(Debug, Parser)] +#[derive(Debug, Parser, PartialEq, Eq)] #[command(name = "dredge", version, author)] #[command(about, long_about)] pub(crate) struct Cli { @@ -71,7 +71,7 @@ impl From for log::LevelFilter { } } -#[derive(Debug, Subcommand)] +#[derive(Debug, Subcommand, PartialEq, Eq)] pub enum Commands { /// Fetch the list of available repositories from the catalog. Catalog, @@ -95,3 +95,157 @@ pub enum Commands { /// Perform a simple version check towards the Docker Registry API Check, } + +#[cfg(test)] +mod tests { + use super::*; + + /// Test that given the --log-level option, ensure that the corresponding + /// `LogLevel` variant is set. + #[test] + fn test_log_level_option_off() { + let args = vec!["dredge", "--log-level=off", "registry.local", "check"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.log_level, LogLevel::Off); + } + + /// Test that given the --log-level option, ensure that the corresponding + /// `LogLevel` variant is set. + #[test] + fn test_log_level_option_trace() { + let args = vec!["dredge", "--log-level=trace", "registry.local", "check"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.log_level, LogLevel::Trace); + } + + /// Test that given the --log-level option, ensure that the corresponding + /// `LogLevel` variant is set. + #[test] + fn test_log_level_option_debug() { + let args = vec!["dredge", "--log-level=debug", "registry.local", "check"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.log_level, LogLevel::Debug); + } + + /// Test that given the --log-level option, ensure that the corresponding + /// `LogLevel` variant is set. + #[test] + fn test_log_level_option_info() { + let args = vec!["dredge", "--log-level=info", "registry.local", "check"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.log_level, LogLevel::Info); + } + + /// Test that given the --log-level option, ensure that the corresponding + /// `LogLevel` variant is set. + #[test] + fn test_log_level_option_warn() { + let args = vec!["dredge", "--log-level=warn", "registry.local", "check"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.log_level, LogLevel::Warn); + } + + /// Test that given the --log-level option, ensure that the corresponding + /// `LogLevel` variant is set. + #[test] + fn test_log_level_option_error() { + let args = vec!["dredge", "--log-level=error", "registry.local", "check"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.log_level, LogLevel::Error); + } + + /// Test that given the argument and the "catalog" command, + /// ensure that the expected values are received. + #[test] + fn test_catalog_command() { + let args = vec!["dredge", "registry.local", "catalog"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.registry, String::from("registry.local")); + assert_eq!(cli.command, Commands::Catalog); + } + + /// Test that given the argument and the "tags" command with a + /// specific image name, the expected values are received. + #[test] + fn test_tags_command() { + let args = vec!["dredge", "registry.local", "tags", "foobar"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.registry, *"registry.local"); + assert_eq!( + cli.command, + Commands::Tags { + name: String::from("foobar") + } + ); + } + + /// Test that given the argument and the "show" command with + /// an image name but no tag, the expected values are received. + #[test] + fn test_show_command() { + let args = vec!["dredge", "registry.local", "show", "foo"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.registry, *"registry.local"); + assert_eq!( + cli.command, + Commands::Show { + image: String::from("foo"), + tag: None, + } + ); + } + + /// Test that given the argument and the "show" command with + /// both an image and tag, the expected values are received. + #[test] + fn test_show_command_with_optional_tag() { + let args = vec!["dredge", "registry.local", "show", "foo", "bar"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.registry, *"registry.local"); + assert_eq!( + cli.command, + Commands::Show { + image: String::from("foo"), + tag: Some(String::from("bar")), + } + ); + } + + /// Test that given the argument and the "delete" command, with + /// both an image and tag, the expected values are received. + #[test] + fn test_delete_command() { + let args = vec!["dredge", "registry.local", "delete", "foo", "bar"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.registry, *"registry.local"); + assert_eq!( + cli.command, + Commands::Delete { + image: String::from("foo"), + tag: String::from("bar"), + } + ); + } + + /// Test that given the argument and the "check" command, the + /// expected values are received. + #[test] + fn test_check_command() { + let args = vec!["dredge", "registry.local", "check"]; + let cli = Cli::parse_from(args); + + assert_eq!(cli.registry, *"registry.local"); + assert_eq!(cli.command, Commands::Check); + } +} diff --git a/src/main.rs b/src/main.rs index 3dcd7e7..09704e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,8 +16,9 @@ #![deny(clippy::pedantic)] -use clap::Parser; use std::io::{self, Write}; + +use clap::Parser; use url::Url; use crate::cli::Cli; @@ -47,9 +48,12 @@ const LATEST: &str = "latest"; fn parse_registry_arg(host: &str) -> Result { log::trace!("make_registry_url(host: {host})"); - Url::parse(host) - .or_else(|_| Url::parse(&format!("https://{host}"))) - .or(Err(DredgeError::RegistryUrlError(host.to_string()))) + let mut host = String::from(host); + if !host.starts_with("http://") && !host.starts_with("https://") { + host = format!("https://{host}"); + } + + Url::parse(&host).or(Err(DredgeError::RegistryUrlError(host.to_string()))) } #[async_std::main] @@ -87,3 +91,70 @@ async fn main() -> Result<(), DredgeError> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Test that given a valid URL in the argument, we return the + /// same URL from `parse_registry_arg()` + #[test] + fn test_parse_valid_url_registry_arg() { + let host = "https://example.com/registry"; + let result = parse_registry_arg(host); + + // Check if the result is Ok and contains the expected URL + assert!(result.is_ok()); + let url = result.unwrap(); + assert_eq!(url.scheme(), "https"); + assert_eq!(url.host_str(), Some("example.com")); + assert_eq!(url.path(), "/registry"); + } + + /// Test that given only an FQDN for a specific host in the + /// argument, we return an HTTPS url with that FQDN as the host. + #[test] + fn test_parse_valid_fqdn_registry_arg() { + let host = "example.com"; + let result = parse_registry_arg(host); + + // Check if the result is Ok and contains the expected URL + assert!(result.is_ok()); + let url = result.unwrap(); + assert_eq!(url.scheme(), "https"); + assert_eq!(url.host_str(), Some("example.com")); + assert_eq!(url.path(), "/"); + } + + /// Test that given an FQDN with port for a specific host in the + /// argument, we return an HTTPS url with that FQDN as the host and the + /// given port as the parsed port number. + #[test] + fn test_parse_valid_fqdn_registry_arg_alt_port() { + let host = "example.com:5123"; + let result = parse_registry_arg(host); + + // Check if the result is Ok and contains the expected URL + assert!(result.is_ok()); + let url = result.unwrap(); + assert_eq!(url.scheme(), "https"); + assert_eq!(url.host_str(), Some("example.com")); + assert_eq!(url.port(), Some(5123)); + assert_eq!(url.path(), "/"); + } + + /// Test that given an arbitrary string which can not be parsed as a valid + /// URL or FQDN, we return the `RegistryUrlError` variant. + #[test] + fn test_parse_invalid_registry_arg() { + let host = "///"; // This is not a valid URL + let result = parse_registry_arg(host); + + // Check if result is Err and matches the expected error variant. + assert!(result.is_err()); + match result { + Err(DredgeError::RegistryUrlError(_)) => {} // Expected error variant, + _ => panic!("Expected RegistryUrlError, got a different error"), + } + } +}