mirror of
https://github.com/anthonyoteri/dredge.git
synced 2026-06-05 15:26:53 -04:00
Merge pull request #35 from anthonyoteri/unit-tests
This commit is contained in:
@@ -18,3 +18,6 @@ thiserror = "1.0.48"
|
|||||||
toml = "0.8.0"
|
toml = "0.8.0"
|
||||||
url = { version = "2.4.1", features = ["serde"] }
|
url = { version = "2.4.1", features = ["serde"] }
|
||||||
xdg = "2.5.2"
|
xdg = "2.5.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
mockito = "1.2.0"
|
||||||
+264
-7
@@ -14,6 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ use crate::error::ApiError;
|
|||||||
///
|
///
|
||||||
/// Returns an `ApiError` if there is a problem fetching or parsing the
|
/// Returns an `ApiError` if there is a problem fetching or parsing the
|
||||||
/// responses from the Docker Registry API.
|
/// responses from the Docker Registry API.
|
||||||
pub async fn catalog_handler(registry_url: &Url) -> Result<(), ApiError> {
|
pub async fn catalog_handler(buf: &mut dyn Write, registry_url: &Url) -> Result<(), ApiError> {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Response {
|
struct Response {
|
||||||
repositories: Vec<String>,
|
repositories: Vec<String>,
|
||||||
@@ -45,7 +47,7 @@ pub async fn catalog_handler(registry_url: &Url) -> Result<(), ApiError> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for repository in repository_list {
|
for repository in repository_list {
|
||||||
println!("{repository}");
|
writeln!(buf, "{repository}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -60,7 +62,11 @@ pub async fn catalog_handler(registry_url: &Url) -> Result<(), ApiError> {
|
|||||||
///
|
///
|
||||||
/// Returns an `ApiError` if there is a problem fetching or parsing the
|
/// Returns an `ApiError` if there is a problem fetching or parsing the
|
||||||
/// responses from the Docker Registry API.
|
/// responses from the Docker Registry API.
|
||||||
pub async fn tags_handler(registry_url: &Url, name: &str) -> Result<(), ApiError> {
|
pub async fn tags_handler(
|
||||||
|
buf: &mut dyn Write,
|
||||||
|
registry_url: &Url,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Response {
|
struct Response {
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
@@ -76,7 +82,7 @@ pub async fn tags_handler(registry_url: &Url, name: &str) -> Result<(), ApiError
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for tag in tag_list {
|
for tag in tag_list {
|
||||||
println!("{tag}");
|
writeln!(buf, "{tag}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -89,7 +95,12 @@ pub async fn tags_handler(registry_url: &Url, name: &str) -> Result<(), ApiError
|
|||||||
/// Returns an `ApiError` if there is a problem fetching the manifest or if there
|
/// Returns an `ApiError` if there is a problem fetching the manifest or if there
|
||||||
/// is a problem parsing the response from the Docker Registry API.
|
/// is a problem parsing the response from the Docker Registry API.
|
||||||
#[allow(clippy::unused_async)]
|
#[allow(clippy::unused_async)]
|
||||||
pub async fn show_handler(registry_url: &Url, image: &str, tag: &str) -> Result<(), ApiError> {
|
pub async fn show_handler(
|
||||||
|
_buf: &mut dyn Write,
|
||||||
|
registry_url: &Url,
|
||||||
|
image: &str,
|
||||||
|
tag: &str,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
log::trace!("show_handler(registry_url: {registry_url:?}, image: {image}, tag: {tag})");
|
log::trace!("show_handler(registry_url: {registry_url:?}, image: {image}, tag: {tag})");
|
||||||
let path = format!("/v2/{image}/manifests/{tag}");
|
let path = format!("/v2/{image}/manifests/{tag}");
|
||||||
let _url = registry_url.join(&path)?;
|
let _url = registry_url.join(&path)?;
|
||||||
@@ -104,7 +115,12 @@ pub async fn show_handler(registry_url: &Url, image: &str, tag: &str) -> Result<
|
|||||||
/// manifest digest, or if there is a problem deleting the manifest from the
|
/// manifest digest, or if there is a problem deleting the manifest from the
|
||||||
/// Docker Registry API.
|
/// Docker Registry API.
|
||||||
#[allow(clippy::unused_async)]
|
#[allow(clippy::unused_async)]
|
||||||
pub async fn delete_handler(registry_url: &Url, image: &str, tag: &str) -> Result<(), ApiError> {
|
pub async fn delete_handler(
|
||||||
|
_buf: &mut dyn Write,
|
||||||
|
registry_url: &Url,
|
||||||
|
image: &str,
|
||||||
|
tag: &str,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
log::trace!("delete_handler(registry_url: {registry_url:?}, image: {image}, tag: {tag})");
|
log::trace!("delete_handler(registry_url: {registry_url:?}, image: {image}, tag: {tag})");
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
@@ -117,7 +133,7 @@ pub async fn delete_handler(registry_url: &Url, image: &str, tag: &str) -> Resul
|
|||||||
///
|
///
|
||||||
/// Returns an `ApiError` if there is a problem communicating with the
|
/// Returns an `ApiError` if there is a problem communicating with the
|
||||||
/// endpoint or if the required version is not supported.
|
/// endpoint or if the required version is not supported.
|
||||||
pub async fn check_handler(registry_url: &Url) -> Result<(), ApiError> {
|
pub async fn check_handler(buf: &mut dyn Write, registry_url: &Url) -> Result<(), ApiError> {
|
||||||
log::trace!("check_handler(registry_url: {registry_url:?})");
|
log::trace!("check_handler(registry_url: {registry_url:?})");
|
||||||
|
|
||||||
let path = "/v2";
|
let path = "/v2";
|
||||||
@@ -125,5 +141,246 @@ pub async fn check_handler(registry_url: &Url) -> Result<(), ApiError> {
|
|||||||
|
|
||||||
let response = reqwest::get(url).await?;
|
let response = reqwest::get(url).await?;
|
||||||
api::parse_response_status(&response)?;
|
api::parse_response_status(&response)?;
|
||||||
|
writeln!(buf, "Ok")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::error;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Validate the happy path for the catalog handler.
|
||||||
|
///
|
||||||
|
/// This test spins up a mock server, and makes a request to the catalog
|
||||||
|
/// endpoint. It checks that the handler both called the request the
|
||||||
|
/// expected number of times, and did not return an error.
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_catalog_handler() {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2/_catalog";
|
||||||
|
|
||||||
|
let registry_url = Url::parse(&server.url()).expect("Failed to parse registry URL");
|
||||||
|
let mock_response = server
|
||||||
|
.mock("GET", path)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_body(r#"{"repositories": ["image1", "image2", "image3"]}"#)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let result = catalog_handler(&mut buf, ®istry_url).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(String::from_utf8(buf).unwrap(), *"image1\nimage2\nimage3\n");
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the pagination of the catalog handler.
|
||||||
|
///
|
||||||
|
/// This test spins up a mock server, and makes a request to the catalog
|
||||||
|
/// endpoint. The response includes a pagination link, which the handler
|
||||||
|
/// should follow, resulting in the combined list. It checks that the
|
||||||
|
/// handler both called the request the expected number of times, and did
|
||||||
|
/// not return an error.
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_catalog_handler_with_pagination() {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2/_catalog";
|
||||||
|
let path2 = "/v2/_catalog?n=2,last=image2";
|
||||||
|
|
||||||
|
let registry_url = Url::parse(&server.url()).expect("Failed to parse registry URL");
|
||||||
|
let mock_response = server
|
||||||
|
.mock("GET", path)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header(
|
||||||
|
http::header::LINK.as_str(),
|
||||||
|
&format!(r#"<{path2}>; rel=next"#),
|
||||||
|
)
|
||||||
|
.with_body(r#"{"repositories": ["image1", "image2"]}"#)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mock_response2 = server
|
||||||
|
.mock("GET", path2)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_body(r#"{"repositories": ["image3"]}"#)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let result = catalog_handler(&mut buf, ®istry_url).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(String::from_utf8(buf).unwrap(), *"image1\nimage2\nimage3\n");
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
mock_response2.assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the happy path for the tags handler.
|
||||||
|
///
|
||||||
|
/// This test spins up a mock server, and makes a request to the tags
|
||||||
|
/// endpoint. It checks that the handler both called the request the
|
||||||
|
/// expected number of times, and did not return an error.
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_tags_handler() {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2/some_image/tags/list";
|
||||||
|
|
||||||
|
// Mock the HTTP response for the Docker Registry API
|
||||||
|
let registry_url = Url::parse(&server.url()).expect("Failed to parse registry URL");
|
||||||
|
let mock_response = server
|
||||||
|
.mock("GET", path)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_body(r#"{"tags": ["tag1", "tag2", "tag3"]}"#)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let result = tags_handler(&mut buf, ®istry_url, "some_image").await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(String::from_utf8(buf).unwrap(), *"tag1\ntag2\ntag3\n");
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the pagination of the catalog handler.
|
||||||
|
///
|
||||||
|
/// This test spins up a mock server, and makes a request to the catalog
|
||||||
|
/// endpoint. The response includes a pagination link, which the handler
|
||||||
|
/// should follow, resulting in the combined list. It checks that the
|
||||||
|
/// handler both called the request the expected number of times, and did
|
||||||
|
/// not return an error.
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_tags_handler_with_pagination() {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2/some_image/tags/list";
|
||||||
|
let path2 = "/v2/some_image/tags/list?n=2,last=tag2";
|
||||||
|
|
||||||
|
// Mock the HTTP response for the Docker Registry API
|
||||||
|
let registry_url = Url::parse(&server.url()).expect("Failed to parse registry URL");
|
||||||
|
let mock_response = server
|
||||||
|
.mock("GET", path)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header(
|
||||||
|
http::header::LINK.as_str(),
|
||||||
|
&format!(r#"<{path2}>; rel=next"#),
|
||||||
|
)
|
||||||
|
.with_body(r#"{"tags": ["tag1", "tag2"]}"#)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mock_response2 = server
|
||||||
|
.mock("GET", path2)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_body(r#"{"tags": ["tag3"]}"#)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let result = tags_handler(&mut buf, ®istry_url, "some_image").await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(String::from_utf8(buf).unwrap(), *"tag1\ntag2\ntag3\n");
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
mock_response2.assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the happy path for the check handler.
|
||||||
|
///
|
||||||
|
/// This test spins up a mock server, and makes a request to the check
|
||||||
|
/// endpoint. It checks that the handler both called the request the
|
||||||
|
/// expected number of times, and did not return an error.
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_check_handler() {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2";
|
||||||
|
|
||||||
|
// Mock the HTTP response for the Docker Registry API
|
||||||
|
let registry_url = Url::parse(&server.url()).expect("Failed to parse registry URL");
|
||||||
|
let mock_response = server
|
||||||
|
.mock("GET", path)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let result = check_handler(&mut buf, ®istry_url).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(String::from_utf8(buf).unwrap(), *"Ok\n");
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the the check handler on invalid API version
|
||||||
|
///
|
||||||
|
/// This validates that if the "Docker-Distribution-API-Version" header
|
||||||
|
/// is missing in the response, the appropriate error is returned.
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_check_handler_missing_api_version() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2";
|
||||||
|
|
||||||
|
// Mock the HTTP response for the Docker Registry API
|
||||||
|
let registry_url = Url::parse(&server.url()).expect("Failed to parse registry URL");
|
||||||
|
let mock_response = server
|
||||||
|
.mock("GET", path)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let result = check_handler(&mut buf, ®istry_url).await;
|
||||||
|
|
||||||
|
// Ensure that we got the correct error type.
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
match err {
|
||||||
|
error::ApiError::UnexpectedResponse(_) => Ok(()),
|
||||||
|
e => Err(e),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the the check handler on invalid API version
|
||||||
|
///
|
||||||
|
/// This validates that if the "Docker-Distribution-API-Version" header
|
||||||
|
/// is present in the response but contains an unexpected value, the
|
||||||
|
/// appropriate error is returned.
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_check_handler_invalid_api_version() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
let path = "/v2";
|
||||||
|
|
||||||
|
// Mock the HTTP response for the Docker Registry API
|
||||||
|
let registry_url = Url::parse(&server.url()).expect("Failed to parse registry URL");
|
||||||
|
let mock_response = server
|
||||||
|
.mock("GET", path)
|
||||||
|
.with_status(http::status::StatusCode::OK.as_u16().into())
|
||||||
|
.with_header(http::header::CONTENT_TYPE.as_str(), "application/json")
|
||||||
|
.with_header("Docker-Distribution-API-Version", "registry/1.0")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let result = check_handler(&mut buf, ®istry_url).await;
|
||||||
|
|
||||||
|
// Ensure that we got the correct error type.
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
match err {
|
||||||
|
error::ApiError::UnsupportedVersion(_) => Ok(()),
|
||||||
|
e => Err(e),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
mock_response.assert();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ pub enum DredgeError {
|
|||||||
/// An error building the registry URL
|
/// An error building the registry URL
|
||||||
#[error("Error determining registry URL from {0}")]
|
#[error("Error determining registry URL from {0}")]
|
||||||
RegistryUrlError(String),
|
RegistryUrlError(String),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error related to the communication with the registry API.
|
/// An error related to the communication with the registry API.
|
||||||
@@ -56,6 +59,9 @@ pub enum ApiError {
|
|||||||
|
|
||||||
#[error("Resource not found")]
|
#[error("Resource not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::header::ToStrError> for ApiError {
|
impl From<reqwest::header::ToStrError> for ApiError {
|
||||||
|
|||||||
+15
-6
@@ -17,6 +17,7 @@
|
|||||||
#![deny(clippy::pedantic)]
|
#![deny(clippy::pedantic)]
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use std::io::{self, Write};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
@@ -63,18 +64,26 @@ async fn main() -> Result<(), DredgeError> {
|
|||||||
let registry_url: Url = parse_registry_arg(&args.registry)?;
|
let registry_url: Url = parse_registry_arg(&args.registry)?;
|
||||||
|
|
||||||
// -- Dispatch control to the appropriate command handler.
|
// -- Dispatch control to the appropriate command handler.
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
match args.command {
|
match args.command {
|
||||||
Commands::Catalog => commands::catalog_handler(®istry_url).await?,
|
Commands::Catalog => commands::catalog_handler(&mut buf, ®istry_url).await?,
|
||||||
Commands::Tags { name } => commands::tags_handler(®istry_url, &name).await?,
|
Commands::Tags { name } => commands::tags_handler(&mut buf, ®istry_url, &name).await?,
|
||||||
Commands::Show { image, tag } => {
|
Commands::Show { image, tag } => {
|
||||||
commands::show_handler(®istry_url, &image, &tag.unwrap_or(LATEST.to_string()))
|
commands::show_handler(
|
||||||
.await?;
|
&mut buf,
|
||||||
|
®istry_url,
|
||||||
|
&image,
|
||||||
|
&tag.unwrap_or(LATEST.to_string()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Commands::Delete { image, tag } => {
|
Commands::Delete { image, tag } => {
|
||||||
commands::delete_handler(®istry_url, &image, &tag).await?;
|
commands::delete_handler(&mut buf, ®istry_url, &image, &tag).await?;
|
||||||
}
|
}
|
||||||
Commands::Check => commands::check_handler(®istry_url).await?,
|
Commands::Check => commands::check_handler(&mut buf, ®istry_url).await?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
io::stdout().write_all(&buf)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user