diff --git a/linkup-cli/src/background_booting.rs b/linkup-cli/src/background_booting.rs index 68dfb71..a085d65 100644 --- a/linkup-cli/src/background_booting.rs +++ b/linkup-cli/src/background_booting.rs @@ -20,13 +20,13 @@ use crate::{CliError, LINKUP_LOCALDNS_INSTALL}; #[cfg_attr(test, mockall::automock)] pub trait BackgroundServices { - fn boot_background_services(&self, state: LocalState) -> Result<(), CliError>; + fn boot_background_services(&self, state: LocalState) -> Result; } pub struct RealBackgroundServices; impl BackgroundServices for RealBackgroundServices { - fn boot_background_services(&self, mut state: LocalState) -> Result<(), CliError> { + fn boot_background_services(&self, mut state: LocalState) -> Result { let local_url = Url::parse(&format!("http://localhost:{}", LINKUP_LOCALSERVER_PORT)) .expect("linkup url invalid"); @@ -75,17 +75,17 @@ impl BackgroundServices for RealBackgroundServices { boot_local_dns(state.domain_strings(), state.linkup.session_name.clone())?; } - // if let Some(tunnel) = &state.linkup.tunnel { - // println!("Waiting for tunnel DNS to propogate at {}...", tunnel); + if let Some(tunnel) = &state.linkup.tunnel { + println!("Waiting for tunnel DNS to propagate at {}...", tunnel); - // wait_for_dns_ok(tunnel.clone())?; + wait_for_dns_ok(tunnel.clone())?; - // println!(); - // } + println!(); + } print_session_names(&state); - Ok(()) + Ok(state) } } diff --git a/linkup-cli/src/file_system.rs b/linkup-cli/src/file_system.rs index 5d0db31..cd6ad2c 100644 --- a/linkup-cli/src/file_system.rs +++ b/linkup-cli/src/file_system.rs @@ -14,6 +14,7 @@ pub trait FileSystem { fn file_exists(&self, file_path: &Path) -> bool; fn create_dir_all(&self, path: &Path) -> Result<(), CliError>; fn get_home(&self) -> Result; + fn get_env(&self, key: &str) -> Result; } pub struct RealFileSystem; @@ -41,4 +42,8 @@ impl FileSystem for RealFileSystem { fn get_home(&self) -> Result { Ok(env::var("HOME").expect("HOME is not set")) } + + fn get_env(&self, key: &str) -> Result { + Ok(env::var(key).unwrap_or_else(|_| panic!("{} is not set", key))) + } } diff --git a/linkup-cli/src/local_config.rs b/linkup-cli/src/local_config.rs index 250c421..4cd3754 100644 --- a/linkup-cli/src/local_config.rs +++ b/linkup-cli/src/local_config.rs @@ -38,6 +38,9 @@ impl LocalState { } pub fn save(&mut self) -> Result<(), CliError> { + if cfg!(test) { + return Ok(()); + } let yaml_string = match serde_yaml::to_string(self) { Ok(yaml) => yaml, Err(_) => { diff --git a/linkup-cli/src/paid_tunnel.rs b/linkup-cli/src/paid_tunnel.rs index 4a0ae4f..234bff2 100644 --- a/linkup-cli/src/paid_tunnel.rs +++ b/linkup-cli/src/paid_tunnel.rs @@ -3,7 +3,7 @@ use std::{env, fs, path::Path}; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; use crate::file_system::{FileLike, FileSystem, RealFileSystem}; -use crate::CliError; +use crate::{CliError, LINKUP_LOCALSERVER_PORT}; use serde::{Deserialize, Serialize}; use base64::prelude::*; @@ -189,7 +189,8 @@ impl PaidTunnelManager for RealPaidTunnelManager { println!("{}", body); - let _parsed : CreateDNSRecordResponse = send_request(&client, &url, headers, Some(body), "POST")?; + let _parsed: CreateDNSRecordResponse = + send_request(&client, &url, headers, Some(body), "POST")?; Ok(()) } } @@ -205,7 +206,8 @@ fn save_tunnel_credentials( tunnel_id: &str, tunnel_secret: &str, ) -> Result<(), CliError> { - let account_id = env::var("LINKUP_CLOUDFLARE_ACCOUNT_ID") + let account_id = filesys + .get_env("LINKUP_CLOUDFLARE_ACCOUNT_ID") .map_err(|err| CliError::BadConfig(err.to_string()))?; let data = serde_json::json!({ "AccountTag": account_id, @@ -214,7 +216,9 @@ fn save_tunnel_credentials( }); // Determine the directory path - let home_dir = env::var("HOME").map_err(|err| CliError::BadConfig(err.to_string()))?; + let home_dir = filesys + .get_env("HOME") + .map_err(|err| CliError::BadConfig(err.to_string()))?; let dir_path = Path::new(&home_dir).join(".cloudflared"); // Create the directory if it does not exist @@ -239,7 +243,9 @@ fn save_tunnel_credentials( fn create_config_yml(filesys: &dyn FileSystem, tunnel_id: &str) -> Result<(), CliError> { // Determine the directory path - let home_dir = env::var("HOME").map_err(|err| CliError::BadConfig(err.to_string()))?; + let home_dir = filesys + .get_env("HOME") + .map_err(|err| CliError::BadConfig(err.to_string()))?; let dir_path = Path::new(&home_dir).join(".cloudflared"); // Create the directory if it does not exist @@ -255,7 +261,7 @@ fn create_config_yml(filesys: &dyn FileSystem, tunnel_id: &str) -> Result<(), Cl let file_path_str = file_path.to_string_lossy().to_string(); let config = Config { - url: "http://localhost:8000".to_string(), + url: format!("http://localhost:{}", LINKUP_LOCALSERVER_PORT), tunnel: tunnel_id.to_string(), credentials_file: file_path_str, }; @@ -325,11 +331,14 @@ mod tests { #[test] fn create_config_yml_when_no_config_dir() { - env::set_var("HOME", "/tmp/home"); - let content = "url: http://localhost:8000\ntunnel: TUNNEL_ID\ncredentials-file: /tmp/home/.cloudflared/TUNNEL_ID.json\n"; + let content = "url: http://localhost:9066\ntunnel: TUNNEL_ID\ncredentials-file: /tmp/home/.cloudflared/TUNNEL_ID.json\n"; let mut mock_fs = MockFileSystem::new(); // If .cloudflared directory does not exist: + mock_fs + .expect_get_env() + .with(predicate::eq("HOME")) + .returning(|_| Ok("/tmp/home".to_string())); mock_fs .expect_file_exists() .withf(|path| path.ends_with(".cloudflared")) @@ -352,16 +361,17 @@ mod tests { let result = create_config_yml(&mock_fs, "TUNNEL_ID"); assert!(result.is_ok()); - - env::remove_var("HOME") } #[test] fn create_config_yml_config_dir_exists() { - env::set_var("HOME", "/tmp/home"); - let content = "url: http://localhost:8000\ntunnel: TUNNEL_ID\ncredentials-file: /tmp/home/.cloudflared/TUNNEL_ID.json\n"; + let content = "url: http://localhost:9066\ntunnel: TUNNEL_ID\ncredentials-file: /tmp/home/.cloudflared/TUNNEL_ID.json\n"; let mut mock_fs = MockFileSystem::new(); + mock_fs + .expect_get_env() + .with(predicate::eq("HOME")) + .returning(|_| Ok("/tmp/home".to_string())); // If .cloudflared directory exists: mock_fs .expect_file_exists() @@ -382,17 +392,23 @@ mod tests { let result = create_config_yml(&mock_fs, "TUNNEL_ID"); assert!(result.is_ok()); - - env::remove_var("HOME") } #[test] fn test_save_tunnel_credentials() { - env::set_var("HOME", "/tmp/home"); - env::set_var("LINKUP_CLOUDFLARE_ACCOUNT_ID", "ACCOUNT_ID"); let content = "{\"AccountTag\":\"ACCOUNT_ID\",\"TunnelID\":\"TUNNEL_ID\",\"TunnelSecret\":\"AQIDBAUGBwgBAgMEBQYHCAECAwQFBgcIAQIDBAUGBwg=\"}"; let mut mock_fs = MockFileSystem::new(); + mock_fs + .expect_get_env() + .with(predicate::eq("HOME")) + .returning(|_| Ok("/tmp/home".to_string())); + + mock_fs + .expect_get_env() + .with(predicate::eq("LINKUP_CLOUDFLARE_ACCOUNT_ID")) + .returning(|_| Ok("ACCOUNT_ID".to_string())); + mock_fs .expect_create_file() .withf(|path| path.ends_with("TUNNEL_ID.json")) @@ -408,9 +424,6 @@ mod tests { "AQIDBAUGBwgBAgMEBQYHCAECAwQFBgcIAQIDBAUGBwg=", ); assert!(result.is_ok()); - - env::remove_var("HOME"); - env::remove_var("LINKUP_CLOUDFLARE_ACCOUNT_ID"); } #[test] diff --git a/linkup-cli/src/services/tunnel.rs b/linkup-cli/src/services/tunnel.rs index f427201..865f3e8 100644 --- a/linkup-cli/src/services/tunnel.rs +++ b/linkup-cli/src/services/tunnel.rs @@ -42,7 +42,7 @@ impl TunnelManager for RealTunnelManager { let mut attempt = 0; loop { attempt += 1; - match try_run_tunnel(&state) { + match try_run_tunnel(state) { Ok(url) => return Ok(url), Err(CliError::StopErr(e)) => { return Err(CliError::StopErr(format!( @@ -80,7 +80,7 @@ fn try_run_tunnel(state: &LocalState) -> Result { match daemonize.execute() { Outcome::Child(child_result) => match child_result { - Ok(_) => daemonized_tunnel_child(&state), + Ok(_) => daemonized_tunnel_child(state), Err(e) => { return Err(CliError::StartLocalTunnel(format!( "Failed to start local tunnel: {}", @@ -99,8 +99,11 @@ fn try_run_tunnel(state: &LocalState) -> Result { }, } + let is_paid = state.is_paid; + let session_name = state.linkup.session_name.clone(); + let tunnel_url_re = - Regex::new(r"Starting tunnel tunnelID=.*").expect("Failed to compile regex"); + Regex::new(r"https://[a-zA-Z0-9-]+\.trycloudflare\.com").expect("Failed to compile regex"); let tunnel_started_re = Regex::new(r"Registered tunnel connection").expect("Failed to compile regex"); @@ -123,7 +126,15 @@ fn try_run_tunnel(state: &LocalState) -> Result { for line in buf_reader.lines() { let line = line.unwrap_or_default(); - if let Some(url_match) = tunnel_url_re.find(&line) { + if is_paid { + url = Some( + Url::parse( + format!("https://tunnel-{}.mentimeter.dev", session_name) + .as_str(), + ) + .expect("Failed to parse tunnel URL"), + ); + } else if let Some(url_match) = tunnel_url_re.find(&line) { let found_url = Url::parse(url_match.as_str()).expect("Failed to parse tunnel URL"); url = Some(found_url); @@ -149,7 +160,6 @@ fn try_run_tunnel(state: &LocalState) -> Result { thread::sleep(Duration::from_millis(100)); } }); - match rx.recv_timeout(Duration::from_secs(TUNNEL_START_WAIT)) { Ok(result) => result, Err(e) => { @@ -166,8 +176,9 @@ fn daemonized_tunnel_child(state: &LocalState) { let url = format!("http://localhost:{}", LINKUP_LOCALSERVER_PORT); let cmd_args: Vec<&str> = match state.is_paid { true => vec!["tunnel", "run", state.linkup.session_name.as_str()], - false => vec!["tunnel", "run", "--url", url.as_str()], + false => vec!["tunnel", "--url", url.as_str()], }; + println!("Starting cloudflared tunnel with args: {:?}", cmd_args); let mut child_cmd = Command::new("cloudflared") .args(cmd_args) .stdout(Stdio::inherit()) diff --git a/linkup-cli/src/start.rs b/linkup-cli/src/start.rs index ca98132..15434aa 100644 --- a/linkup-cli/src/start.rs +++ b/linkup-cli/src/start.rs @@ -3,10 +3,8 @@ use std::{ path::{Path, PathBuf}, }; -use url::Url; - use crate::{ - background_booting::{load_config, BackgroundServices, RealBackgroundServices, ServerConfig}, + background_booting::{BackgroundServices, RealBackgroundServices}, env_files::write_to_env_file, file_system::{FileSystem, RealFileSystem}, linkup_file_path, @@ -14,7 +12,6 @@ use crate::{ paid_tunnel::{PaidTunnelManager, RealPaidTunnelManager}, services::tunnel::{RealTunnelManager, TunnelManager}, LINKUP_LOCALDNS_INSTALL, - LINKUP_LOCALSERVER_PORT }; use crate::{ local_config::LocalState, @@ -29,6 +26,7 @@ pub fn start(config_arg: &Option, no_tunnel: bool) -> Result<(), CliErro start_paid_tunnel( &RealPaidTunnelManager, &RealFileSystem, + &RealBackgroundServices, &RealTunnelManager, state, )?; @@ -47,16 +45,16 @@ fn use_paid_tunnels() -> bool { fn start_paid_tunnel( paid_manager: &dyn PaidTunnelManager, filesys: &dyn FileSystem, + boot: &dyn BackgroundServices, tunnel_manager: &dyn TunnelManager, mut state: LocalState, ) -> Result<(), CliError> { - let boot = RealBackgroundServices {}; - boot.boot_background_services(state.clone())?; - - state = LocalState::load()?; - + state = boot.boot_background_services(state.clone())?; - println!("Starting paid tunnel with session name: {}", state.linkup.session_name); + println!( + "Starting paid tunnel with session name: {}", + state.linkup.session_name + ); let tunnel_name = state.linkup.session_name.to_string(); let tunnel_id = match paid_manager.get_tunnel_id(&tunnel_name) { Ok(Some(id)) => id, @@ -72,7 +70,9 @@ fn start_paid_tunnel( let file_path = format!("{}/.cloudflared/{}.json", filesys.get_home()?, tunnel_id); if filesys.file_exists(Path::new(&file_path)) { println!("File exists: {}", file_path); - tunnel_manager.run_tunnel(&state)?; + let tunnel = tunnel_manager.run_tunnel(&state)?; + state.linkup.tunnel = Some(tunnel); + state.save()?; return Ok(()); } } @@ -102,7 +102,7 @@ fn start_free_tunnel(state: LocalState, no_tunnel: bool) -> Result<(), CliError> return Err(CliError::NoTunnelWithoutLocalDns); } - let background_service = RealBackgroundServices; + let background_service = RealBackgroundServices {}; background_service.boot_background_services(state)?; check_local_not_started()?; @@ -193,19 +193,26 @@ fn check_local_not_started() -> Result<(), CliError> { #[cfg(test)] mod tests { + use mockall::mock; + use crate::{ - file_system::MockFileSystem, local_config::LinkupState, paid_tunnel::MockPaidTunnelManager, + background_booting::MockBackgroundServices, file_system::MockFileSystem, + local_config::LinkupState, paid_tunnel::MockPaidTunnelManager, services::tunnel::MockTunnelManager, }; + use url::Url; + use super::*; - #[test] - fn test_start_paid_tunnel_tunnel_exists() { - let mut mock_paid_manager = MockPaidTunnelManager::new(); - let mut mock_fs = MockFileSystem::new(); - let mut mock_tunnel_manager = MockTunnelManager::new(); - let mocked_state: LocalState = LocalState { + mock! { + pub LocalState { + pub fn save(&self) -> Result<(), CliError>; + } + } + + fn make_state() -> LocalState { + return LocalState { linkup: { LinkupState { session_name: "test_session".to_string(), @@ -220,6 +227,15 @@ mod tests { domains: vec![], is_paid: true, }; + } + + #[test] + fn test_start_paid_tunnel_tunnel_exists() { + let mut mock_boot_bg_services = MockBackgroundServices::new(); + let mut mock_paid_manager = MockPaidTunnelManager::new(); + let mut mock_fs = MockFileSystem::new(); + let mut mock_tunnel_manager = MockTunnelManager::new(); + mock_paid_manager .expect_get_tunnel_id() .returning(|_| Ok(Some("test_tunnel_id".to_string()))); @@ -229,6 +245,10 @@ mod tests { .returning(|| Ok("/tmp/home".to_string())); mock_paid_manager.expect_create_tunnel().never(); mock_paid_manager.expect_create_dns_record().never(); + mock_boot_bg_services + .expect_boot_background_services() + .times(1) + .returning(|_| Ok(make_state())); mock_tunnel_manager .expect_run_tunnel() .times(1) @@ -236,31 +256,23 @@ mod tests { let _result = start_paid_tunnel( &mock_paid_manager, &mock_fs, + &mock_boot_bg_services, &mock_tunnel_manager, - mocked_state, + make_state(), ); } #[test] fn test_start_paid_tunnel_no_tunnel_exists() { + let mut mock_boot_bg_services = MockBackgroundServices::new(); let mut mock_manager = MockPaidTunnelManager::new(); let mut mock_fs = MockFileSystem::new(); let mut mock_tunnel_manager = MockTunnelManager::new(); - let mocked_state: LocalState = LocalState { - linkup: { - LinkupState { - session_name: "test_session".to_string(), - session_token: "test_token".to_string(), - config_path: "/tmp/home/.linkup/config".to_string(), - remote: Url::parse("http://localhost:9066").unwrap(), - tunnel: None, - cache_routes: None, - } - }, - services: vec![], - domains: vec![], - is_paid: true, - }; + + mock_boot_bg_services + .expect_boot_background_services() + .times(1) + .returning(|_| Ok(make_state())); mock_manager.expect_get_tunnel_id().returning(|_| Ok(None)); mock_fs.expect_file_exists().never(); mock_manager @@ -275,7 +287,12 @@ mod tests { .expect_run_tunnel() .times(1) .returning(|_| Ok(Url::parse("http://localhost:9066").unwrap())); - let _result = - start_paid_tunnel(&mock_manager, &mock_fs, &mock_tunnel_manager, mocked_state); + let _result = start_paid_tunnel( + &mock_manager, + &mock_fs, + &mock_boot_bg_services, + &mock_tunnel_manager, + make_state(), + ); } }