From b4d1a6c6c05ebd17cf407a923eb45266f05392b2 Mon Sep 17 00:00:00 2001 From: Michael Lieberman Date: Wed, 21 Feb 2024 04:46:18 +0000 Subject: [PATCH] Add tests for skootrs This is an initial set of tests for the core functionality of skootrs. There is more to be done, especially in making sure these tests can run inside of github actions that might not have go, maven, etc. --- .gitignore | 3 +- Cargo.lock | 96 ++++++-- shell.nix | 1 + skootrs-lib/Cargo.toml | 4 + skootrs-lib/src/service/ecosystem.rs | 64 +++++ skootrs-lib/src/service/project.rs | 336 ++++++++++++++++++++++++--- skootrs-lib/src/service/repo.rs | 37 ++- skootrs-lib/src/service/source.rs | 94 +++++++- 8 files changed, 581 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index 0dc232e..f0135e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target/ .vscode .DS_Store -state.db \ No newline at end of file +state.db +.envrc \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e0903b9..341c657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "sha1", "smallvec", "tokio", @@ -1544,6 +1544,12 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "2.0.0" @@ -2755,7 +2761,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -2834,7 +2840,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -3053,7 +3059,7 @@ dependencies = [ "opentelemetry", "ordered-float 4.2.0", "percent-encoding", - "rand", + "rand 0.8.5", "thiserror", "tokio", "tokio-stream", @@ -3131,7 +3137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3478,6 +3484,19 @@ dependencies = [ "serde", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -3486,7 +3505,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3496,9 +3515,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.4" @@ -3508,6 +3542,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3608,6 +3651,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rend" version = "0.4.1" @@ -3796,7 +3848,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -3883,7 +3935,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand", + "rand 0.8.5", "rkyv", "serde", "serde_json", @@ -4395,7 +4447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -4459,6 +4511,8 @@ dependencies = [ "serde_json", "serde_yaml", "skootrs-model", + "tempdir", + "tokio", "tracing", "utoipa", ] @@ -4726,7 +4780,7 @@ dependencies = [ "pharos", "pin-project-lite", "radix_trie", - "rand", + "rand 0.8.5", "regex", "reqwest", "revision", @@ -4778,7 +4832,7 @@ dependencies = [ "getrandom", "hmac", "pem 2.0.1", - "rand", + "rand 0.8.5", "ring 0.16.20", "rsa", "serde", @@ -4848,6 +4902,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + [[package]] name = "term" version = "0.7.0" @@ -4966,9 +5030,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -5282,7 +5346,7 @@ dependencies = [ "http 0.2.11", "httparse", "log", - "rand", + "rand 0.8.5", "rustls", "sha1", "thiserror", @@ -5322,7 +5386,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e37c4b6cbcc59a8dcd09a6429fbc7890286bcbb79215cea7b38a3c4c0921d93" dependencies = [ - "rand", + "rand 0.8.5", "serde", ] diff --git a/shell.nix b/shell.nix index e5589a5..8f0c260 100644 --- a/shell.nix +++ b/shell.nix @@ -7,6 +7,7 @@ rustup bunyan-rs go + maven ]; RUSTC_VERSION = pkgs.lib.readFile ./rust-toolchain; # https://github.com/rust-lang/rust-bindgen#environment-variables diff --git a/skootrs-lib/Cargo.toml b/skootrs-lib/Cargo.toml index d424981..983a538 100644 --- a/skootrs-lib/Cargo.toml +++ b/skootrs-lib/Cargo.toml @@ -19,3 +19,7 @@ tracing = "0.1" futures = "0.3.30" skootrs-model = { path = "../skootrs-model" } ahash = "0.8.7" + +[dev-dependencies] +tempdir = "0.3.7" +tokio = { version = "1.36.0", features = ["rt", "macros"] } diff --git a/skootrs-lib/src/service/ecosystem.rs b/skootrs-lib/src/service/ecosystem.rs index 19d8841..5bb175e 100644 --- a/skootrs-lib/src/service/ecosystem.rs +++ b/skootrs-lib/src/service/ecosystem.rs @@ -110,3 +110,67 @@ impl LocalGoEcosystemHandler { } } } + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + + #[test] + fn test_local_maven_ecosystem_handler_initialize_success() { + let temp_dir = TempDir::new("test").unwrap(); + let path = temp_dir.path().to_str().unwrap(); + let params = MavenParams { + group_id: "com.example".to_string(), + artifact_id: "my-project".to_string(), + }; + + let result = LocalMavenEcosystemHandler::initialize(path, ¶ms); + + assert!(result.is_ok()); + } + + #[test] + fn test_local_maven_ecosystem_handler_initialize_failure() { + let temp_dir = TempDir::new("test").unwrap(); + let path = temp_dir.path().to_str().unwrap(); + let params = MavenParams { + // Invalid group ID + group_id: "".to_string(), + artifact_id: "my-project".to_string(), + }; + + let result = LocalMavenEcosystemHandler::initialize(path, ¶ms); + + assert!(result.is_err()); + } + + #[test] + fn test_local_go_ecosystem_handler_initialize_success() { + let temp_dir = TempDir::new("test").unwrap(); + let path = temp_dir.path().to_str().unwrap(); + let params = GoParams { + name: "my-project".to_string(), + host: "github.com".to_string(), + }; + + let result = LocalGoEcosystemHandler::initialize(path, ¶ms); + + assert!(result.is_ok()); + } + + #[test] + fn test_local_go_ecosystem_handler_initialize_failure() { + let temp_dir = TempDir::new("test").unwrap(); + let path = temp_dir.path().to_str().unwrap(); + let params = GoParams { + // Invalid project name + name: "".to_string(), + host: "github.com".to_string(), + }; + + let result = LocalGoEcosystemHandler::initialize(path, ¶ms); + + assert!(result.is_err()); + } +} diff --git a/skootrs-lib/src/service/project.rs b/skootrs-lib/src/service/project.rs index c85deeb..6a66b5a 100644 --- a/skootrs-lib/src/service/project.rs +++ b/skootrs-lib/src/service/project.rs @@ -1,4 +1,3 @@ - // // Copyright 2024 The Skootrs Authors. // @@ -16,44 +15,60 @@ use std::error::Error; -use skootrs_model::skootrs::{facet::CommonFacetParams, InitializedProject, InitializedSource, ProjectParams}; use crate::service::facet::{FacetSetParamsGenerator, RootFacetService}; +use skootrs_model::skootrs::{ + facet::CommonFacetParams, InitializedProject, InitializedSource, ProjectParams, +}; -use super::{repo::{LocalRepoService, RepoService}, ecosystem::{LocalEcosystemService, EcosystemService}, source::{LocalSourceService, SourceService}, facet::LocalFacetService}; +use super::{ + ecosystem::EcosystemService, + repo::RepoService, + source::SourceService, +}; use tracing::debug; /// The `ProjectService` trait provides an interface for initializing and managing a Skootrs project. pub trait ProjectService { - fn initialize(&self, params: ProjectParams) -> impl std::future::Future>> + Send; + fn initialize( + &self, + params: ProjectParams, + ) -> impl std::future::Future>> + Send; } /// The `LocalProjectService` struct provides an implementation of the `ProjectService` trait for initializing /// and managing a Skootrs project on the local machine. #[derive(Debug)] -pub struct LocalProjectService { - pub repo_service: LocalRepoService, - pub ecosystem_service: LocalEcosystemService, - pub source_service: LocalSourceService, - pub facet_service: LocalFacetService, +pub struct LocalProjectService { + pub repo_service: RS, + pub ecosystem_service: ES, + pub source_service: SS, + pub facet_service: FS, } -impl ProjectService for LocalProjectService { - async fn initialize(&self, params: ProjectParams) -> Result> { - // TODO: The octocrab initialization should be done in a better place and be parameterized - let o: octocrab::Octocrab = octocrab::Octocrab::builder() - .personal_token( - std::env::var("GITHUB_TOKEN") - .expect("GITHUB_TOKEN env var must be populated") - ) - .build()? - ; - octocrab::initialise(o); +impl ProjectService for LocalProjectService +where + RS: RepoService + Send + Sync, + ES: EcosystemService + Send + Sync, + SS: SourceService + Send + Sync, + FS: RootFacetService + Send + Sync, +{ + async fn initialize( + &self, + params: ProjectParams, + ) -> Result> { debug!("Starting repo initialization"); - let initialized_repo = self.repo_service.initialize(params.repo_params.clone()).await?; + let initialized_repo = self + .repo_service + .initialize(params.repo_params.clone()) + .await?; debug!("Starting source initialization"); - let initialized_source: InitializedSource = self.source_service.initialize(params.source_params.clone(), initialized_repo.clone())?; + let initialized_source: InitializedSource = self + .source_service + .initialize(params.source_params.clone(), initialized_repo.clone())?; debug!("Starting ecosystem initialization"); - let initialized_ecosystem = self.ecosystem_service.initialize(params.ecosystem_params.clone(), initialized_source.clone())?; + let initialized_ecosystem = self + .ecosystem_service + .initialize(params.ecosystem_params.clone(), initialized_source.clone())?; debug!("Starting facet initialization"); // TODO: This is ugly and this should probably be configured somewhere better, preferably outside of code. let facet_set_params_generator = FacetSetParamsGenerator {}; @@ -64,12 +79,23 @@ impl ProjectService for LocalProjectService { ecosystem: initialized_ecosystem.clone(), }; //let facet_set_params = facet_set_params_generator.generate_default(&common_params)?; - let source_facet_set_params = facet_set_params_generator.generate_default_source_bundle(&common_params)?; - let api_facet_set_params = facet_set_params_generator.generate_default_api_bundle(&common_params)?; - let initialized_source_facets = self.facet_service.initialize_all(source_facet_set_params).await?; + let source_facet_set_params = + facet_set_params_generator.generate_default_source_bundle(&common_params)?; + let api_facet_set_params = + facet_set_params_generator.generate_default_api_bundle(&common_params)?; + let initialized_source_facets = self + .facet_service + .initialize_all(source_facet_set_params) + .await?; // TODO: Figure out how to better order commits and pushes - self.source_service.commit_and_push_changes(initialized_source.clone(), "Initialized project".to_string())?; - let initialized_api_facets = self.facet_service.initialize_all(api_facet_set_params).await?; + self.source_service.commit_and_push_changes( + initialized_source.clone(), + "Initialized project".to_string(), + )?; + let initialized_api_facets = self + .facet_service + .initialize_all(api_facet_set_params) + .await?; let initialized_facets = [initialized_source_facets, initialized_api_facets].concat(); debug!("Completed project initialization"); @@ -81,4 +107,256 @@ impl ProjectService for LocalProjectService { facets: initialized_facets, }) } -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use skootrs_model::skootrs::{ + facet::{ + APIBundleFacet, APIContent, FacetParams, FacetSetParams, InitializedFacet, + SourceBundleFacet, SourceFileContent, SupportedFacetType, + }, EcosystemParams, GithubRepoParams, GithubUser, GoParams, InitializedEcosystem, InitializedGithubRepo, InitializedGo, InitializedMaven, InitializedRepo, RepoParams, SkootError, SourceParams + }; + + use super::*; + struct MockRepoService; + struct MockEcosystemService; + struct MockSourceService; + struct MockFacetService; + + impl RepoService for MockRepoService { + fn initialize(&self, params: RepoParams) -> impl std::future::Future> + Send { + async { + let inner_params = match params { + RepoParams::Github(g) => g, + }; + + // Special case for testing error handling + if inner_params.name == "error" { + return Err("Error".into()) + } + + let initialized_repo = InitializedRepo::Github(InitializedGithubRepo { + name: inner_params.name, + organization: inner_params.organization, + }); + + Ok(initialized_repo) + } + } + + fn clone_local( + &self, + initialized_repo: InitializedRepo, + path: String, + ) -> Result { + let inner_repo = match initialized_repo { + InitializedRepo::Github(g) => g, + }; + + if inner_repo.name == "error" { + return Err("Error".into()); + } + + let initialized_source = InitializedSource { + path: format!("{}/{}", path, inner_repo.name), + }; + + Ok(initialized_source) + } + } + + impl EcosystemService for MockEcosystemService { + fn initialize( + &self, + params: EcosystemParams, + _source: InitializedSource, + ) -> Result { + let initialized_ecosystem = match params { + EcosystemParams::Go(g) => { + if g.host == "error" { + return Err("Error".into()); + } + InitializedEcosystem::Go(InitializedGo{ + name: g.name, + host: g.host, + }) + } + EcosystemParams::Maven(m) => { + if m.group_id == "error" { + return Err("Error".into()); + } + InitializedEcosystem::Maven(InitializedMaven{ + group_id: m.group_id, + artifact_id: m.artifact_id, + }) + } + }; + + Ok(initialized_ecosystem) + } + } + + impl SourceService for MockSourceService { + fn initialize( + &self, + params: skootrs_model::skootrs::SourceParams, + initialized_repo: InitializedRepo, + ) -> Result { + if params.parent_path == "error" { + return Err("Error".into()); + } + + let repo_name = match initialized_repo { + InitializedRepo::Github(g) => g.name, + }; + + let initialized_source = InitializedSource { + path: format!("{}/{}", params.parent_path, repo_name), + }; + + Ok(initialized_source) + } + + fn commit_and_push_changes( + &self, + source: InitializedSource, + message: String, + ) -> Result<(), SkootError> { + if message == "error" { + return Err("Error".into()); + } + + Ok(()) + } + + fn write_file, C: AsRef<[u8]>>( + &self, + _source: InitializedSource, + _path: P, + name: String, + _contents: C, + ) -> Result<(), SkootError> { + if name == "error" { + return Err("Error".into()); + } + + Ok(()) + } + + fn read_file>( + &self, + _source: &InitializedSource, + _path: P, + name: String, + ) -> Result { + if name == "error" { + return Err("Error".into()); + } + + Ok("Worked".to_string()) + } + } + + impl RootFacetService for MockFacetService { + fn initialize( + &self, + params: FacetParams, + ) -> impl std::future::Future> + Send + { + async { + match params { + FacetParams::SourceFile(_) => Err("Error".into()), + FacetParams::SourceBundle(s) => { + if s.common.project_name == "error" { + return Err("Error".into()); + } + let source_bundle_facet = SourceBundleFacet { + source_files: vec![SourceFileContent { + name: "README.md".to_string(), + path: "./".to_string(), + content: s.common.project_name.clone(), + }], + facet_type: SupportedFacetType::Readme, + }; + + Ok(InitializedFacet::SourceBundle(source_bundle_facet)) + } + FacetParams::APIBundle(a) => { + if a.common.project_name == "error" { + return Err("Error".into()); + } + let api_bundle_facet = APIBundleFacet { + apis: vec![APIContent { + name: "test".to_string(), + url: "https://foo.bar/test".to_string(), + response: "worked".to_string(), + }], + facet_type: SupportedFacetType::BranchProtection, + }; + + Ok(InitializedFacet::APIBundle(api_bundle_facet)) + } + } + } + } + + fn initialize_all( + &self, + params: FacetSetParams, + ) -> impl std::future::Future, SkootError>> + Send + { + async { + let mut initialized_facets = Vec::new(); + for facet_params in params.facets_params { + let initialized_facet = self.initialize(facet_params).await?; + initialized_facets.push(initialized_facet); + } + + Ok(initialized_facets) + } + } + } + + #[tokio::test] + async fn test_initialize_project() { + let project_params = ProjectParams { + name: "test".to_string(), + repo_params: RepoParams::Github(GithubRepoParams { + name: "test".to_string(), + description: "foobar".to_string(), + organization: GithubUser::User("testuser".to_string()) + }), + ecosystem_params: EcosystemParams::Go(GoParams { + name: "test".to_string(), + host: "github.com".to_string() + }), + source_params: SourceParams { + parent_path: "test".to_string() + } + }; + + let local_project_service = LocalProjectService { + repo_service: MockRepoService, + ecosystem_service: MockEcosystemService, + source_service: MockSourceService, + facet_service: MockFacetService, + }; + + let result = local_project_service.initialize(project_params).await; + + assert!(result.is_ok()); + let initialized_project = result.unwrap(); + + assert!(initialized_project.repo.full_url() == "https://github.com/testuser/test"); + let module = match initialized_project.ecosystem { + InitializedEcosystem::Go(g) => g, + _ => panic!("Wrong ecosystem type"), + }; + assert!(module.name == "test"); + assert!(initialized_project.source.path == "test/test"); + // TODO: This just pulls in the default set of facets which has a length of 12. + // This should be more configurable. + assert_eq!(initialized_project.facets.len(), 12); + } +} diff --git a/skootrs-lib/src/service/repo.rs b/skootrs-lib/src/service/repo.rs index fac0fea..373060e 100644 --- a/skootrs-lib/src/service/repo.rs +++ b/skootrs-lib/src/service/repo.rs @@ -38,6 +38,13 @@ pub struct LocalRepoService {} impl RepoService for LocalRepoService { async fn initialize(&self, params: RepoParams) -> Result { + // TODO: The octocrab initialization should be done in a better place and be parameterized + let o: octocrab::Octocrab = octocrab::Octocrab::builder() + .personal_token( + std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN env var must be populated"), + ) + .build()?; + octocrab::initialise(o); match params { RepoParams::Github(g) => { let github_repo_handler = GithubRepoHandler { @@ -141,4 +148,32 @@ struct NewGithubRepoParams { has_issues: bool, has_projects: bool, has_wiki: bool, -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use tempdir::TempDir; + + use super::*; + + // TODO: Mock out, or create test to create a repo/delete a repo + + #[test] + fn test_clone_local_github_repo() { + let initialized_github_repo = InitializedGithubRepo { + name: "skootrs".to_string(), + organization: GithubUser::Organization("kusaridev".to_string()), + }; + + let temp_dir = TempDir::new("test").unwrap(); + let path = temp_dir.path().to_str().unwrap(); + let result = GithubRepoHandler::clone_local(&initialized_github_repo, path); + assert!(result.is_ok()); + + let initialized_source = result.unwrap(); + assert_eq!( + initialized_source.path, + format!("{}/{}", path, initialized_github_repo.name) + ); + } +} diff --git a/skootrs-lib/src/service/source.rs b/skootrs-lib/src/service/source.rs index 6a7d144..b57c28c 100644 --- a/skootrs-lib/src/service/source.rs +++ b/skootrs-lib/src/service/source.rs @@ -15,14 +15,13 @@ #![allow(clippy::module_name_repetitions)] -use std::{error::Error, process::Command, path::Path, fs}; +use std::{error::Error, fs, path::Path, process::Command}; -use tracing::{info, debug}; +use tracing::{debug, info}; use skootrs_model::skootrs::{InitializedRepo, InitializedSource, SkootError, SourceParams}; use super::repo::{LocalRepoService, RepoService}; - /// The `SourceService` trait provides an interface for and managing a project's source code. /// This code is usually something a local git repo. The service differs from the repo service /// in that it's focused on the files and not the repo itself. @@ -32,7 +31,11 @@ pub trait SourceService { params: SourceParams, initialized_repo: InitializedRepo, ) -> Result; - fn commit_and_push_changes(&self, source: InitializedSource, message: String) -> Result<(), SkootError>; + fn commit_and_push_changes( + &self, + source: InitializedSource, + message: String, + ) -> Result<(), SkootError>; fn write_file, C: AsRef<[u8]>>( &self, source: InitializedSource, @@ -40,7 +43,12 @@ pub trait SourceService { name: String, contents: C, ) -> Result<(), SkootError>; - fn read_file>(&self, source: &InitializedSource, path: P, name: String) -> Result; + fn read_file>( + &self, + source: &InitializedSource, + path: P, + name: String, + ) -> Result; } /// The `LocalSourceService` struct provides an implementation of the `SourceService` trait for initializing @@ -60,7 +68,11 @@ impl SourceService for LocalSourceService { repo_service.clone_local(initialized_repo, params.parent_path) } - fn commit_and_push_changes(&self, source: InitializedSource, message: String) -> Result<(), Box> { + fn commit_and_push_changes( + &self, + source: InitializedSource, + message: String, + ) -> Result<(), Box> { let _output = Command::new("git") .arg("add") .arg(".") @@ -102,9 +114,77 @@ impl SourceService for LocalSourceService { Ok(()) } - fn read_file>(&self, source: &InitializedSource, path: P, name: String) -> Result { + fn read_file>( + &self, + source: &InitializedSource, + path: P, + name: String, + ) -> Result { let full_path = Path::new(&source.path).join(&path).join(name); let contents = fs::read_to_string(full_path)?; Ok(contents) } } + +#[cfg(test)] +mod tests { + use super::*; + use skootrs_model::skootrs::{GithubUser, InitializedGithubRepo, InitializedRepo, InitializedSource, SourceParams}; + use std::path::PathBuf; + use tempdir::TempDir; + + #[test] + fn test_initialize() { + let source_service = LocalSourceService {}; + let temp_dir = TempDir::new("test").unwrap(); + let parent_path = temp_dir.path().to_str().unwrap(); + let params = SourceParams { + parent_path: parent_path.to_string(), + }; + let initialized_repo = InitializedRepo::Github( + InitializedGithubRepo { + name: "skootrs".to_string(), + organization: GithubUser::Organization("kusaridev".to_string()), + }); + let result = source_service.initialize(params, initialized_repo); + assert!(result.is_ok()); + let initialized_source = result.unwrap(); + assert_eq!(initialized_source.path, format!("{}/{}", parent_path, "skootrs")); + } + + #[test] + fn test_write_file() { + let source_service = LocalSourceService {}; + let temp_dir = TempDir::new("test").unwrap(); + let initialized_source = InitializedSource { + path: temp_dir.path().to_str().unwrap().to_string(), + }; + let path = "subdirectory"; + let name = "file.txt".to_string(); + let contents = "File contents".as_bytes(); + let result = source_service.write_file(initialized_source, path, name.clone(), contents); + assert!(result.is_ok()); + let file_path = PathBuf::from(format!("{}/{}", temp_dir.path().to_str().unwrap(), path)).join(name); + assert!(file_path.exists()); + let file_contents = fs::read_to_string(file_path).unwrap(); + assert_eq!(file_contents, "File contents"); + } + + #[test] + fn test_read_file() { + let source_service = LocalSourceService {}; + let temp_dir = TempDir::new("test").unwrap(); + let initialized_source = InitializedSource { + path: temp_dir.path().to_str().unwrap().to_string(), + }; + let path = "subdirectory"; + let name = "file.txt".to_string(); + let contents = "File contents".as_bytes(); + let result = source_service.write_file(initialized_source.clone(), path, name.clone(), contents); + assert!(result.is_ok()); + let file_path = PathBuf::from(format!("{}/{}", temp_dir.path().to_str().unwrap(), path)).join(name.clone()); + assert!(file_path.exists()); + let file_contents = source_service.read_file(&initialized_source, path, name).unwrap(); + assert_eq!(file_contents, "File contents"); + } +}