From ca4a7b3466707dffb3eb5a39fab71eb96b38aee8 Mon Sep 17 00:00:00 2001 From: Robert Lawson <63192607+Robert-maker1994@users.noreply.github.com> Date: Sun, 23 Feb 2025 11:06:06 +0100 Subject: [PATCH] test: add full integration test for tuono new CLI command (#411) Co-authored-by: Valerio Ageno --- crates/tuono/Cargo.toml | 2 + crates/tuono/src/scaffold_project.rs | 64 +++++++---- crates/tuono/src/source_builder.rs | 2 - crates/tuono/tests/cli_build.rs | 7 +- crates/tuono/tests/cli_new.rs | 61 +++++++++++ .../tuono/tests/utils/mock_github_endpoint.rs | 101 ++++++++++++++++++ crates/tuono/tests/utils/mod.rs | 2 + .../{utils.rs => utils/temp_tuono_project.rs} | 1 + 8 files changed, 211 insertions(+), 29 deletions(-) create mode 100644 crates/tuono/tests/cli_new.rs create mode 100644 crates/tuono/tests/utils/mock_github_endpoint.rs create mode 100644 crates/tuono/tests/utils/mod.rs rename crates/tuono/tests/{utils.rs => utils/temp_tuono_project.rs} (98%) diff --git a/crates/tuono/Cargo.toml b/crates/tuono/Cargo.toml index 36eddaf9..5bc0fb5a 100644 --- a/crates/tuono/Cargo.toml +++ b/crates/tuono/Cargo.toml @@ -37,6 +37,8 @@ spinners = "4.1.1" console = "0.15.10" [dev-dependencies] +wiremock = "0.6.2" tempfile = "3.14.0" assert_cmd = "2.0.16" serial_test = "0.10.0" + diff --git a/crates/tuono/src/scaffold_project.rs b/crates/tuono/src/scaffold_project.rs index 2a8c7890..0e4b30de 100644 --- a/crates/tuono/src/scaffold_project.rs +++ b/crates/tuono/src/scaffold_project.rs @@ -7,13 +7,6 @@ use std::fs::{self, create_dir, File, OpenOptions}; use std::io::{self, prelude::*}; use std::path::{Path, PathBuf}; -const GITHUB_TUONO_TAGS_URL: &str = "https://api.github.com/repos/tuono-labs/tuono/git/ref/tags/"; - -const GITHUB_TUONO_TAG_COMMIT_TREES_URL: &str = - "https://api.github.com/repos/tuono-labs/tuono/git/trees/"; - -const GITHUB_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/tuono-labs/tuono"; - #[derive(Deserialize, Debug)] enum GithubFileType { #[serde(rename = "blob")] @@ -69,6 +62,12 @@ pub fn create_new_project( ) { let folder = folder_name.unwrap_or(".".to_string()); + let github_api_base_url = + env::var("__INTERNAL_TUONO_TEST").unwrap_or("https://api.github.com".to_string()); + + let github_raw_base_url = env::var("__INTERNAL_TUONO_TEST") + .unwrap_or("https://raw.githubusercontent.com".to_string()); + // In case of missing select the tuono example let template = template.unwrap_or("tuono-app".to_string()); let client = blocking::Client::builder() @@ -79,7 +78,8 @@ pub fn create_new_project( // This string does not include the "v" version prefix let cli_version: &str = crate_version!(); - let tree_url: String = generate_tree_url(select_head, &client, cli_version); + let tree_url: String = + generate_tree_url(select_head, &client, cli_version, &github_api_base_url); let res_tree = client .get(tree_url) @@ -126,7 +126,8 @@ pub fn create_new_project( } in new_project_files.iter() { if let GithubFileType::Blob = element_type { - let url = generate_raw_content_url(select_head, cli_version, path); + let url = + generate_raw_content_url(select_head, cli_version, path, &github_raw_base_url); let file_content = client .get(url) @@ -144,7 +145,7 @@ pub fn create_new_project( let file_path = folder_path.join(&path); if let Err(err) = create_file(file_path, file_content) { - exit_with_error(&format!("Failed to create file: {}", err)); + exit_with_error(&format!("Failed to create file: {err}")); } } } @@ -154,22 +155,34 @@ pub fn create_new_project( outro(folder); } -fn generate_raw_content_url(select_head: Option, cli_version: &str, path: &String) -> String { +fn generate_raw_content_url( + select_head: Option, + cli_version: &str, + path: &String, + url: &str, +) -> String { let tag = if select_head.unwrap_or(false) { - "main" + "/main" } else { - &format!("v{cli_version}") + &format!("/tuono-labs/tuono/v{cli_version}") }; - format!("{}/{}/{}", GITHUB_RAW_CONTENT_URL, tag, path) + format!("{url}{tag}/{path}") } -fn generate_tree_url(select_head: Option, client: &Client, cli_version: &str) -> String { +fn generate_tree_url( + select_head: Option, + client: &Client, + cli_version: &str, + url: &str, +) -> String { if select_head.unwrap_or(false) { - format!("{}main?recursive=1", GITHUB_TUONO_TAG_COMMIT_TREES_URL) + format!("{url}/repos/tuono-labs/tuono/git/trees/main?recursive=1") } else { // This string does not include the "v" version prefix let res_tag = client - .get(format!("{}v{}", GITHUB_TUONO_TAGS_URL, cli_version)) + .get(format!( + "{url}/repos/tuono-labs/tuono/git/ref/tags/v{cli_version}" + )) .send() .unwrap_or_else(|_| { exit_with_error("Failed to call the tag github API for v{cli_version}") @@ -178,8 +191,8 @@ fn generate_tree_url(select_head: Option, client: &Client, cli_version: &s .unwrap_or_else(|_| exit_with_error("Failed to parse the tag response")); format!( - "{}{}?recursive=1", - GITHUB_TUONO_TAG_COMMIT_TREES_URL, res_tag.object.sha + "{url}/repos/tuono-labs/tuono/git/trees/{}?recursive=1", + res_tag.object.sha ) } } @@ -197,7 +210,10 @@ fn create_directories( let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), "")); let dir_path = folder_path.join(&path); - create_dir(&dir_path).unwrap(); + if let Err(e) = create_dir(&dir_path) { + eprintln!("Failed to create directory {}: {}", dir_path.display(), e); + std::process::exit(1); + } } } Ok(()) @@ -267,12 +283,13 @@ mod tests { fn generate_valid_content_url_from_head() { let expected = format!( "{}/{}/{}", - GITHUB_RAW_CONTENT_URL, "main", "examples/tuono-app" + "http://localhost:3000", "main", "examples/tuono-app" ); let generated = generate_raw_content_url( Some(true), crate_version!(), &String::from("examples/tuono-app"), + "http://localhost:3000", ); assert_eq!(expected, generated) } @@ -281,14 +298,15 @@ mod tests { fn generate_valid_content_url_from_cli_version() { let expected = format!( "{}/{}/{}", - GITHUB_RAW_CONTENT_URL, - &format!("v{}", crate_version!()), + "http://localhost:3000", + &format!("tuono-labs/tuono/v{}", crate_version!()), "examples/tuono-app" ); let generated = generate_raw_content_url( Some(false), crate_version!(), &String::from("examples/tuono-app"), + "http://localhost:3000", ); assert_eq!(expected, generated) } diff --git a/crates/tuono/src/source_builder.rs b/crates/tuono/src/source_builder.rs index 195a5295..2244edad 100644 --- a/crates/tuono/src/source_builder.rs +++ b/crates/tuono/src/source_builder.rs @@ -127,9 +127,7 @@ pub fn bundle_axum_source(mode: Mode) -> io::Result { let base_path = std::env::current_dir().unwrap(); let app = App::new(); - let bundled_file = generate_axum_source(&app, mode); - create_main_file(&base_path, &bundled_file); Ok(app) diff --git a/crates/tuono/tests/cli_build.rs b/crates/tuono/tests/cli_build.rs index 92e4a70d..c807e471 100644 --- a/crates/tuono/tests/cli_build.rs +++ b/crates/tuono/tests/cli_build.rs @@ -1,8 +1,9 @@ -mod utils; use assert_cmd::Command; use serial_test::serial; use std::fs; -use utils::TempTuonoProject; + +mod utils; +use utils::temp_tuono_project::TempTuonoProject; const POST_API_FILE: &str = r"#[tuono_lib::api(POST)]"; const GET_API_FILE: &str = r"#[tuono_lib::api(GET)]"; @@ -58,8 +59,6 @@ fn it_successfully_create_an_api_route() { let temp_main_rs_content = fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content."); - dbg!(&temp_main_rs_content); - assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/health_check.rs"]"#)); assert!(temp_main_rs_content.contains("mod api_health_check;")); diff --git a/crates/tuono/tests/cli_new.rs b/crates/tuono/tests/cli_new.rs new file mode 100644 index 00000000..e88eaf4c --- /dev/null +++ b/crates/tuono/tests/cli_new.rs @@ -0,0 +1,61 @@ +use assert_cmd::Command; +use clap::crate_version; +use serial_test::serial; + +use std::fs; + +mod utils; + +use utils::mock_github_endpoint::GitHubServerMock; +use utils::temp_tuono_project::TempTuonoProject; + +#[tokio::test] +#[serial] +async fn it_creates_a_new_project_and_replace_the_versions_in_the_manifest() { + let GitHubServerMock { env_vars, .. } = GitHubServerMock::new().await; + + let temp_project = TempTuonoProject::new(); + + let project_folder = "my-project"; + + let mut cmd = Command::cargo_bin("tuono").unwrap(); + + cmd.arg("new") + .arg(project_folder) + .envs(env_vars) + .assert() + .success(); + + let main_rs_path = temp_project + .path() + .join(project_folder) + .join("src") + .join("main.rs"); + + let cargo_toml_path = temp_project.path().join(project_folder).join("Cargo.toml"); + let package_json_path = temp_project + .path() + .join(project_folder) + .join("package.json"); + + let main_rs_content = fs::read_to_string(main_rs_path).unwrap(); + let cargo_toml_content = fs::read_to_string(cargo_toml_path).unwrap(); + let package_json_content = fs::read_to_string(package_json_path).unwrap(); + + assert_eq!( + main_rs_content, + "fn main() { println!(\"Hello, world!\"); }" + ); + + let version = crate_version!(); + + let expected_cargo_toml_content = + format!("[package] name = \"tuono-tutorial\" [dependencies] tuono_lib = \"{version}\""); + + assert_eq!(cargo_toml_content, expected_cargo_toml_content); + + let expected_package_json_content = + format!("{{\"name\": \"tuono-app\", \"dependencies\": {{ \"tuono\": \"{version}\" }}}}"); + + assert_eq!(package_json_content, expected_package_json_content); +} diff --git a/crates/tuono/tests/utils/mock_github_endpoint.rs b/crates/tuono/tests/utils/mock_github_endpoint.rs new file mode 100644 index 00000000..c1c8c0ab --- /dev/null +++ b/crates/tuono/tests/utils/mock_github_endpoint.rs @@ -0,0 +1,101 @@ +use clap::crate_version; +use std::env; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +#[allow(dead_code)] +pub struct GitHubServerMock { + pub server: MockServer, + pub env_vars: Vec<(String, String)>, +} + +#[allow(dead_code)] +impl GitHubServerMock { + pub async fn new() -> Self { + let server = MockServer::start().await; + + let tuono_version = crate_version!(); + + let sha = "1234567890abcdef"; + + let sha_response_template = ResponseTemplate::new(200).set_body_raw( + format!("{{ \"object\": {{ \"sha\": \"{sha}\" }} }}"), + "application/json", + ); + + Mock::given(method("GET")) + .and(path(&format!( + "repos/tuono-labs/tuono/git/ref/tags/v{}", + tuono_version + ))) + .respond_with(sha_response_template) + .mount(&server) + .await; + + let tree_response_template = ResponseTemplate::new(200).set_body_raw( + r#"{ + "tree": [ + { + "path": "examples/tuono-app/src", + "type": "tree" + }, + { + "path": "examples/tuono-app/src/main.rs", + "type": "blob" + }, + { + "path": "examples/tuono-app/Cargo.toml", + "type": "blob" + }, + { + "path": "examples/tuono-app/package.json", + "type": "blob" + } + ] + }"#, + "application/json", + ); + + Mock::given(method("GET")) + .and(path(&format!("repos/tuono-labs/tuono/git/trees/{}", sha))) + .respond_with(tree_response_template) + .mount(&server) + .await; + + let file_response_template = + |content: &str| ResponseTemplate::new(200).set_body_raw(content, "text/plain"); + + Mock::given(method("GET")) + .and(path(format!( + "tuono-labs/tuono/v{tuono_version}/examples/tuono-app/src/main.rs" + ))) + .respond_with(file_response_template( + "fn main() { println!(\"Hello, world!\"); }", + )) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path(format!( + "tuono-labs/tuono/v{tuono_version}/examples/tuono-app/Cargo.toml" + ))) + .respond_with(file_response_template( + "[package] name = \"tuono-tutorial\" [dependencies] tuono_lib = { path = \"../../crates/tuono_lib/\" }" + )) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path(format!( + "tuono-labs/tuono/v{tuono_version}/examples/tuono-app/package.json" + ))) + .respond_with(file_response_template( + r#"{"name": "tuono-app", "dependencies": { "tuono": "link:../../packages/tuono" }}"#, + )) + .mount(&server) + .await; + + let env_vars = vec![("__INTERNAL_TUONO_TEST".to_string(), server.uri())]; + GitHubServerMock { server, env_vars } + } +} diff --git a/crates/tuono/tests/utils/mod.rs b/crates/tuono/tests/utils/mod.rs new file mode 100644 index 00000000..7d110963 --- /dev/null +++ b/crates/tuono/tests/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod mock_github_endpoint; +pub mod temp_tuono_project; diff --git a/crates/tuono/tests/utils.rs b/crates/tuono/tests/utils/temp_tuono_project.rs similarity index 98% rename from crates/tuono/tests/utils.rs rename to crates/tuono/tests/utils/temp_tuono_project.rs index 87b8a167..3adba32f 100644 --- a/crates/tuono/tests/utils.rs +++ b/crates/tuono/tests/utils/temp_tuono_project.rs @@ -11,6 +11,7 @@ pub struct TempTuonoProject { temp_dir: TempDir, } +#[allow(dead_code)] impl TempTuonoProject { pub fn new() -> Self { let project = TempTuonoProject::new_with_no_config();