diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2018-06-20 08:10:30 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2018-06-20 08:10:30 -0300 |
| commit | 3e7a83f27a10cf6630ba5ff885b64cc24b02e9e3 (patch) | |
| tree | 674ed8c397d5cfb0ba847fd562376f78ca5cbcf6 | |
| parent | 33f015b5c94c9900db5f9e7f7296f57e759094c5 (diff) | |
| parent | e92c41b73121d491899f63e480505959a7d15f99 (diff) | |
| download | mullvadvpn-3e7a83f27a10cf6630ba5ff885b64cc24b02e9e3.tar.xz mullvadvpn-3e7a83f27a10cf6630ba5ff885b64cc24b02e9e3.zip | |
Merge branch 'mock-openvpn'
| -rw-r--r-- | .travis.yml | 8 | ||||
| -rw-r--r-- | Cargo.lock | 17 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | README.md | 7 | ||||
| -rwxr-xr-x | integration-tests.sh | 13 | ||||
| -rw-r--r-- | mullvad-daemon/Cargo.toml | 5 | ||||
| -rw-r--r-- | mullvad-daemon/tests/common/mod.rs | 131 | ||||
| -rw-r--r-- | mullvad-ipc-client/src/lib.rs | 63 | ||||
| -rw-r--r-- | mullvad-tests/Cargo.toml | 21 | ||||
| -rw-r--r-- | mullvad-tests/src/bin/mock_openvpn.rs | 58 | ||||
| -rw-r--r-- | mullvad-tests/src/lib.rs | 273 | ||||
| -rw-r--r-- | mullvad-tests/tests/connection.rs | 76 | ||||
| -rw-r--r-- | mullvad-tests/tests/startup.rs (renamed from mullvad-daemon/tests/startup.rs) | 34 |
13 files changed, 544 insertions, 163 deletions
diff --git a/.travis.yml b/.travis.yml index 6749e60138..80554d2727 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,20 +52,24 @@ matrix: - rustup component add rustfmt-preview - rustfmt --version - cargo fmt -- --check --unstable-features + - ./integration-tests.sh - language: rust rust: beta os: linux cache: cargo before_script: *rust_before_script - script: *rust_script + script: &rust_linux_script + - cargo build --verbose + - cargo test --verbose + - ./integration-tests.sh - language: rust rust: stable os: linux cache: cargo before_script: *rust_before_script - script: *rust_script + script: *rust_linux_script notifications: diff --git a/Cargo.lock b/Cargo.lock index 94737eb8cc..10cb559070 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,11 +750,9 @@ dependencies = [ name = "mullvad-daemon" version = "0.1.0" dependencies = [ - "assert_matches 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", "ctrlc 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "duct 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "fern 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", @@ -769,7 +767,6 @@ dependencies = [ "mullvad-paths 0.1.0", "mullvad-rpc 0.1.0", "mullvad-types 0.1.0", - "os_pipe 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", @@ -830,6 +827,20 @@ dependencies = [ ] [[package]] +name = "mullvad-tests" +version = "0.1.0" +dependencies = [ + "duct 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "mullvad-ipc-client 0.1.0", + "mullvad-paths 0.1.0", + "mullvad-types 0.1.0", + "notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "os_pipe 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "mullvad-types" version = "0.1.0" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml index 8d2f2ded90..80ab5c0a1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "mullvad-paths", "mullvad-types", "mullvad-rpc", + "mullvad-tests", "talpid-openvpn-plugin", "talpid-core", "talpid-ipc", @@ -171,6 +171,13 @@ the version of the app you are going to release. For example `2018.3-beta1` or ` Please pay attention to the output at the end of the script and make sure the version it says it built matches what you want to release. +## Running Integration Tests + +The integration tests are located in the `mullvad-tests` crate. It uses a mock OpenVPN binary to +test the `mullvad-daemon`. To run the tests, the `mullvad-daemon` binary must be built first. +Afterwards, the tests should be executed with the `integration-tests` feature enabled. To simplify +this procedure, the `integration-tests.sh` script can be used to run all integration tests. + ## Command line tools for Electron GUI app development diff --git a/integration-tests.sh b/integration-tests.sh new file mode 100755 index 0000000000..cfb5cdfdd8 --- /dev/null +++ b/integration-tests.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +MULLVAD_DIR="$(cd "$(dirname "$0")"; pwd -P)" + +pushd "$MULLVAD_DIR" + +cargo build \ + && cd mullvad-tests \ + && cargo test --features "integration-tests" + +RESULT="$?" +popd +exit "$RESULT" diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index 1c567052d1..e2120d34b5 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -42,8 +42,3 @@ simple-signal = "1.1" ctrlc = "3.0" windows-service = "0.1" winapi = "0.3" - -[dev-dependencies] -assert_matches = "1.0" -duct = "0.10" -os_pipe = "0.6" diff --git a/mullvad-daemon/tests/common/mod.rs b/mullvad-daemon/tests/common/mod.rs deleted file mode 100644 index d5492ced47..0000000000 --- a/mullvad-daemon/tests/common/mod.rs +++ /dev/null @@ -1,131 +0,0 @@ -#![allow(dead_code)] - -#[cfg(unix)] -extern crate libc; -#[cfg(not(unix))] -extern crate mullvad_ipc_client; - -use std::fs::File; -use std::io::{BufRead, BufReader, Write}; -use std::path::Path; -use std::sync::{mpsc, Arc, Mutex}; -use std::thread; -use std::time::Duration; - -use duct; -use os_pipe::{pipe, PipeReader}; - -#[cfg(unix)] -pub static DAEMON_EXECUTABLE_PATH: &str = "../target/debug/mullvad-daemon"; - -#[cfg(not(unix))] -pub static DAEMON_EXECUTABLE_PATH: &str = r"..\target\debug\mullvad-daemon.exe"; - -fn prepare_relay_list<T: AsRef<Path>>(path: T) { - let path = path.as_ref(); - - if !path.exists() { - File::create(path) - .expect("failed to create relay list file") - .write_all(b"{ \"countries\": [] }") - .expect("failed to write relay list"); - } -} - -pub struct DaemonRunner { - process: Option<duct::Handle>, - output: Arc<Mutex<BufReader<PipeReader>>>, -} - -impl DaemonRunner { - pub fn spawn() -> Self { - prepare_relay_list("../dist-assets/relays.json"); - - let (reader, writer) = pipe().expect("failed to open pipe to connect to daemon"); - let process = cmd!(DAEMON_EXECUTABLE_PATH, "-v", "--disable-log-to-file") - .dir("..") - .env("MULLVAD_CACHE_DIR", "./") - .env("MULLVAD_RESOURCE_DIR", "./dist-assets") - .stderr_to_stdout() - .stdout_handle(writer) - .start() - .expect("failed to start daemon"); - - DaemonRunner { - process: Some(process), - output: Arc::new(Mutex::new(BufReader::new(reader))), - } - } - - pub fn assert_output(&mut self, pattern: &'static str, timeout: Duration) { - let (tx, rx) = mpsc::channel(); - let stdout = self.output.clone(); - - thread::spawn(move || { - Self::wait_for_output(stdout, pattern); - tx.send(()).expect("failed to report search result"); - }); - - rx.recv_timeout(timeout) - .expect(&format!("failed to search for {:?}", pattern)); - } - - fn wait_for_output(output: Arc<Mutex<BufReader<PipeReader>>>, pattern: &str) { - let mut output = output - .lock() - .expect("another thread panicked while holding a lock to the process output"); - - let mut line = String::new(); - - while !line.contains(pattern) { - line.clear(); - output - .read_line(&mut line) - .expect("failed to read line from daemon stdout"); - } - } - - #[cfg(unix)] - fn request_clean_shutdown(&mut self, process: &mut duct::Handle) -> bool { - use duct::unix::HandleExt; - - process.send_signal(libc::SIGTERM).is_ok() - } - - #[cfg(not(unix))] - fn request_clean_shutdown(&mut self, _: &mut duct::Handle) -> bool { - use self::mullvad_ipc_client::DaemonRpcClient; - - if let Ok(mut rpc_client) = DaemonRpcClient::new() { - rpc_client.shutdown().is_ok() - } else { - false - } - } -} - -impl Drop for DaemonRunner { - fn drop(&mut self) { - if let Some(mut process) = self.process.take() { - if self.request_clean_shutdown(&mut process) { - let process = Arc::new(process); - let wait_handle = process.clone(); - let (finished_tx, finished_rx) = mpsc::channel(); - - thread::spawn(move || finished_tx.send(wait_handle.wait().map(|_| ())).unwrap()); - - let has_finished = finished_rx - .recv_timeout(Duration::from_secs(5)) - .map_err(|_| ()) - .and_then(|result| result.map_err(|_| ())) - .is_ok(); - - if !has_finished { - process.kill().unwrap(); - } - } else { - process.kill().unwrap(); - } - } - } -} diff --git a/mullvad-ipc-client/src/lib.rs b/mullvad-ipc-client/src/lib.rs index 6c1ade7fb1..a88a666307 100644 --- a/mullvad-ipc-client/src/lib.rs +++ b/mullvad-ipc-client/src/lib.rs @@ -9,6 +9,7 @@ extern crate talpid_types; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; +use std::sync::mpsc; use mullvad_types::account::{AccountData, AccountToken}; use mullvad_types::location::GeoIpLocation; @@ -59,6 +60,11 @@ error_chain! { display("Failed to call RPC method \"{}\"", method) } + RpcSubscribeError(event: String) { + description("Failed to subscribe to RPC event") + display("Failed to subscribe to RPC event \"{}\"", event) + } + StartRpcClient(address: String) { description("Failed to start RPC client") display("Failed to start RPC client to {}", address) @@ -77,7 +83,24 @@ pub struct DaemonRpcClient { impl DaemonRpcClient { pub fn new() -> Result<Self> { - let (address, credentials) = Self::read_rpc_file()?; + Self::with_rpc_address_file(mullvad_paths::get_rpc_address_path()?) + } + + pub fn with_rpc_address_file<P: AsRef<Path>>(file_path: P) -> Result<Self> { + ensure_written_by_admin(&file_path)?; + + let (address, credentials) = Self::read_rpc_file(file_path)?; + + Self::with_address_and_credentials(address, credentials) + } + + pub fn with_insecure_rpc_address_file<P: AsRef<Path>>(file_path: P) -> Result<Self> { + let (address, credentials) = Self::read_rpc_file(file_path)?; + + Self::with_address_and_credentials(address, credentials) + } + + fn with_address_and_credentials(address: String, credentials: String) -> Result<Self> { let rpc_client = WsIpcClient::connect(&address).chain_err(|| ErrorKind::StartRpcClient(address))?; let mut instance = DaemonRpcClient { rpc_client }; @@ -89,24 +112,25 @@ impl DaemonRpcClient { Ok(instance) } - fn read_rpc_file() -> Result<(String, String)> { - let file_path = mullvad_paths::get_rpc_address_path()?; + fn read_rpc_file<P>(file_path: P) -> Result<(String, String)> + where + P: AsRef<Path>, + { + let file_path = file_path.as_ref(); let rpc_file = - File::open(&file_path).chain_err(|| ErrorKind::ReadRpcFileError(file_path.clone()))?; - - ensure_written_by_admin(&file_path)?; + File::open(file_path).chain_err(|| ErrorKind::ReadRpcFileError(file_path.to_owned()))?; let reader = BufReader::new(rpc_file); let mut lines = reader.lines(); let address = lines .next() - .ok_or_else(|| ErrorKind::EmptyRpcFile(file_path.clone()))? - .chain_err(|| ErrorKind::ReadRpcFileError(file_path.clone()))?; + .ok_or_else(|| ErrorKind::EmptyRpcFile(file_path.to_owned()))? + .chain_err(|| ErrorKind::ReadRpcFileError(file_path.to_owned()))?; let credentials = lines .next() - .ok_or_else(|| ErrorKind::MissingRpcCredentials(file_path.clone()))? - .chain_err(|| ErrorKind::ReadRpcFileError(file_path.clone()))?; + .ok_or_else(|| ErrorKind::MissingRpcCredentials(file_path.to_owned()))? + .chain_err(|| ErrorKind::ReadRpcFileError(file_path.to_owned()))?; Ok((address, credentials)) } @@ -192,6 +216,25 @@ impl DaemonRpcClient { .call(method, args) .chain_err(|| ErrorKind::RpcCallError(method.to_owned())) } + + pub fn new_state_subscribe(&mut self) -> Result<mpsc::Receiver<DaemonState>> { + self.subscribe("new_state") + } + + pub fn subscribe<T>(&mut self, event: &str) -> Result<mpsc::Receiver<T>> + where + T: for<'de> serde::Deserialize<'de> + Send + 'static, + { + let (event_tx, event_rx) = mpsc::channel(); + let subscribe_method = format!("{}_subscribe", event); + let unsubscribe_method = format!("{}_unsubscribe", event); + + self.rpc_client + .subscribe::<T, T>(subscribe_method, unsubscribe_method, event_tx) + .chain_err(|| ErrorKind::RpcSubscribeError(event.to_owned()))?; + + Ok(event_rx) + } } #[cfg(unix)] diff --git a/mullvad-tests/Cargo.toml b/mullvad-tests/Cargo.toml new file mode 100644 index 0000000000..b114245c32 --- /dev/null +++ b/mullvad-tests/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mullvad-tests" +version = "0.1.0" +authors = ["Mullvad VPN <admin@mullvad.net>", "Janito Vaqueiro Ferreira Filho <janito@mullvad.net>"] +description = "Mullvad test specific modules and binaries" +license = "GPL-3.0" + +[features] +integration-tests = [] + +[dependencies] +duct = "0.10" +mullvad-ipc-client = { path = "../mullvad-ipc-client" } +mullvad-paths = { path = "../mullvad-paths" } +mullvad-types = { path = "../mullvad-types" } +notify = "4.0" +os_pipe = "0.6" +tempfile = "3.0" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/mullvad-tests/src/bin/mock_openvpn.rs b/mullvad-tests/src/bin/mock_openvpn.rs new file mode 100644 index 0000000000..b5c1ff8be4 --- /dev/null +++ b/mullvad-tests/src/bin/mock_openvpn.rs @@ -0,0 +1,58 @@ +extern crate notify; + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::mpsc; + +use notify::{raw_watcher, RawEvent, RecursiveMode, Watcher}; + +fn main() { + let (file, path) = create_args_file(); + + write_command_line(file); + wait_for_file_to_be_deleted(path); +} + +fn create_args_file() -> (File, PathBuf) { + let path = PathBuf::from( + env::var_os("MOCK_OPENVPN_ARGS_FILE").expect("Missing mock OpenVPN arguments file path"), + ); + let file = File::create(&path).expect("Failed to create mock OpenVPN arguments file"); + + (file, path) +} + +fn write_command_line(mut file: File) { + for argument in env::args() { + let escaped_argument = argument + .replace("\\", "\\\\") + .replace("\n", "\\n") + .replace("\r", "\\r"); + + writeln!(file, "{}", escaped_argument).expect("Failed to write argument to file"); + } +} + +fn wait_for_file_to_be_deleted<P: AsRef<Path>>(file: P) { + let file = file.as_ref(); + let (tx, rx) = mpsc::channel(); + + let mut watcher = raw_watcher(tx).expect(&format!( + "Failed to create file watcher for \"{}\"", + file.display() + )); + + watcher + .watch(&file, RecursiveMode::NonRecursive) + .expect(&format!("Failed to watch file: {}", file.display())); + + for event in rx { + if let RawEvent { op: Ok(op), .. } = event { + if op.contains(notify::op::REMOVE) { + break; + } + } + } +} diff --git a/mullvad-tests/src/lib.rs b/mullvad-tests/src/lib.rs new file mode 100644 index 0000000000..35d50e814c --- /dev/null +++ b/mullvad-tests/src/lib.rs @@ -0,0 +1,273 @@ +#![allow(dead_code)] + +#[macro_use] +extern crate duct; +#[cfg(unix)] +extern crate libc; +extern crate mullvad_ipc_client; +extern crate mullvad_paths; +extern crate notify; +extern crate os_pipe; +extern crate tempfile; + +use std::fs::{self, File}; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; + +use self::mullvad_ipc_client::DaemonRpcClient; +use self::notify::{op, RawEvent, RecursiveMode, Watcher}; +use self::os_pipe::{pipe, PipeReader}; +use self::tempfile::TempDir; + +use self::platform_specific::*; + +pub const MOCK_OPENVPN_ARGS_FILE: &str = "mock_openvpn_args"; + +#[cfg(unix)] +mod platform_specific { + pub const DAEMON_EXECUTABLE_PATH: &str = "../target/debug/mullvad-daemon"; + pub const MOCK_OPENVPN_EXECUTABLE_PATH: &str = "../target/debug/mock_openvpn"; + pub const OPENVPN_EXECUTABLE_FILE: &str = "openvpn"; + #[cfg(target_os = "linux")] + pub const TALPID_OPENVPN_PLUGIN_FILE: &str = "libtalpid_openvpn_plugin.so"; + #[cfg(target_os = "macos")] + pub const TALPID_OPENVPN_PLUGIN_FILE: &str = "libtalpid_openvpn_plugin.dylib"; +} + +#[cfg(not(unix))] +mod platform_specific { + pub const DAEMON_EXECUTABLE_PATH: &str = r"..\target\debug\mullvad-daemon.exe"; + pub const MOCK_OPENVPN_EXECUTABLE_PATH: &str = "../target/debug/mock_openvpn.exe"; + pub const OPENVPN_EXECUTABLE_FILE: &str = "openvpn.exe"; + pub const TALPID_OPENVPN_PLUGIN_FILE: &str = "talpid_openvpn_plugin.dll"; +} + +pub fn wait_for_file_write_finish<P: AsRef<Path>>(file_path: P, timeout: Duration) { + let file_path = file_path.as_ref(); + let parent_dir = file_path.parent().expect("Missing file parent directory"); + + let absolute_parent_dir = parent_dir + .canonicalize() + .expect("Failed to get absolute path to watch"); + let file_name = file_path + .file_name() + .expect("Missing file name of file path to watch"); + let absolute_file_path = absolute_parent_dir.join(file_name); + + let (tx, rx) = mpsc::channel(); + let mut watcher = notify::raw_watcher(tx).expect("Failed to listen for file system events"); + let start = Instant::now(); + let mut remaining_time = Some(timeout); + + watcher + .watch(absolute_parent_dir, RecursiveMode::NonRecursive) + .expect("Failed to listen for file system events on directory"); + + if !file_path.exists() { + while let Some(wait_time) = remaining_time { + let event = rx.recv_timeout(wait_time); + + if let Ok(RawEvent { + path: Some(path), + op: Ok(op), + .. + }) = event + { + if op.contains(op::CLOSE_WRITE) && path == absolute_file_path { + break; + } + } + + remaining_time = timeout.checked_sub(start.elapsed()); + } + } +} + +fn prepare_test_dirs() -> (TempDir, PathBuf, PathBuf, PathBuf) { + let temp_dir = TempDir::new().expect("Failed to create temporary daemon directory"); + let cache_dir = temp_dir.path().join("cache"); + let resource_dir = temp_dir.path().join("resource-dir"); + let settings_dir = temp_dir.path().join("settings"); + let openvpn_binary = resource_dir.join(OPENVPN_EXECUTABLE_FILE); + let talpid_openvpn_plugin = resource_dir.join(TALPID_OPENVPN_PLUGIN_FILE); + + fs::create_dir(&cache_dir).expect("Failed to create cache directory"); + fs::create_dir(&resource_dir).expect("Failed to create resource directory"); + fs::create_dir(&settings_dir).expect("Failed to create settings directory"); + + fs::copy(MOCK_OPENVPN_EXECUTABLE_PATH, openvpn_binary) + .expect("Failed to copy mock OpenVPN binary"); + File::create(talpid_openvpn_plugin).expect("Failed to create mock Talpid OpenVPN plugin"); + + prepare_relay_list(resource_dir.join("relays.json")); + + (temp_dir, cache_dir, resource_dir, settings_dir) +} + +fn prepare_relay_list<T: AsRef<Path>>(path: T) { + fs::write( + path, + r#"{ + "countries": [{ + "name": "Mockland", + "code": "fake", + "latitude": -91, + "longitude": 0, + "relays": [{ + "hostname": "fake-mockland", + "ipv4_addr_in": "192.168.0.100", + "ipv4_addr_exit": "192.168.0.101", + "include_in_country": true, + "weight": 100, + "tunnels": { + "openvpn": [ { "port": 10000, "protocol": "udp" } ], + "wireguard": [], + }, + }], + }] + }"#, + ).expect("Failed to create mock relay list file"); +} + +pub struct DaemonRunner { + process: Option<duct::Handle>, + output: Arc<Mutex<BufReader<PipeReader>>>, + mock_openvpn_args_file: PathBuf, + rpc_address_file: PathBuf, + _temp_dir: TempDir, +} + +impl DaemonRunner { + pub fn spawn_with_real_rpc_address_file() -> Self { + Self::spawn_internal(false) + } + + pub fn spawn() -> Self { + Self::spawn_internal(true) + } + + fn spawn_internal(mock_rpc_address_file: bool) -> Self { + let (temp_dir, cache_dir, resource_dir, settings_dir) = prepare_test_dirs(); + let mock_openvpn_args_file = temp_dir.path().join(MOCK_OPENVPN_ARGS_FILE); + let rpc_address_file = if mock_rpc_address_file { + temp_dir.path().join(".mullvad_rpc_address") + } else { + mullvad_paths::get_rpc_address_path().expect("Failed to build RPC connection file path") + }; + + let (reader, writer) = pipe().expect("Failed to open pipe to connect to daemon"); + let mut expression = cmd!(DAEMON_EXECUTABLE_PATH, "-v", "--disable-log-to-file") + .dir("..") + .env("MULLVAD_CACHE_DIR", cache_dir) + .env("MULLVAD_RESOURCE_DIR", resource_dir) + .env("MULLVAD_SETTINGS_DIR", settings_dir) + .env("MOCK_OPENVPN_ARGS_FILE", mock_openvpn_args_file.clone()) + .stderr_to_stdout() + .stdout_handle(writer); + + if mock_rpc_address_file { + expression = expression.env( + "MULLVAD_RPC_ADDRESS_PATH", + rpc_address_file.display().to_string(), + ); + } + + let process = expression.start().expect("Failed to start daemon"); + + DaemonRunner { + process: Some(process), + output: Arc::new(Mutex::new(BufReader::new(reader))), + mock_openvpn_args_file, + rpc_address_file, + _temp_dir: temp_dir, + } + } + + pub fn mock_openvpn_args_file(&self) -> &Path { + &self.mock_openvpn_args_file + } + + pub fn assert_output(&mut self, pattern: &'static str, timeout: Duration) { + let (tx, rx) = mpsc::channel(); + let stdout = self.output.clone(); + + thread::spawn(move || { + Self::wait_for_output(stdout, pattern); + tx.send(()).expect("Failed to report search result"); + }); + + rx.recv_timeout(timeout) + .expect(&format!("failed to search for {:?}", pattern)); + } + + fn wait_for_output(output: Arc<Mutex<BufReader<PipeReader>>>, pattern: &str) { + let mut output = output + .lock() + .expect("Another thread panicked while holding a lock to the process output"); + + let mut line = String::new(); + + while !line.contains(pattern) { + line.clear(); + output + .read_line(&mut line) + .expect("Failed to read line from daemon stdout"); + } + } + + pub fn rpc_client(&mut self) -> Result<DaemonRpcClient, String> { + if !self.rpc_address_file.exists() { + wait_for_file_write_finish(&self.rpc_address_file, Duration::from_secs(10)); + } + + DaemonRpcClient::with_insecure_rpc_address_file(&self.rpc_address_file) + .map_err(|error| format!("Failed to create RPC client: {}", error)) + } + + #[cfg(unix)] + fn request_clean_shutdown(&mut self, process: &mut duct::Handle) -> bool { + use duct::unix::HandleExt; + + process.send_signal(libc::SIGTERM).is_ok() + } + + #[cfg(not(unix))] + fn request_clean_shutdown(&mut self, _: &mut duct::Handle) -> bool { + if let Ok(mut rpc_client) = self.rpc_client() { + rpc_client.shutdown().is_ok() + } else { + false + } + } +} + +impl Drop for DaemonRunner { + fn drop(&mut self) { + if let Some(mut process) = self.process.take() { + if self.request_clean_shutdown(&mut process) { + let process = Arc::new(process); + let wait_handle = process.clone(); + let (finished_tx, finished_rx) = mpsc::channel(); + + thread::spawn(move || finished_tx.send(wait_handle.wait().map(|_| ())).unwrap()); + + let has_finished = finished_rx + .recv_timeout(Duration::from_secs(5)) + .map_err(|_| ()) + .and_then(|result| result.map_err(|_| ())) + .is_ok(); + + if !has_finished { + process.kill().unwrap(); + } + } else { + process.kill().unwrap(); + } + } + + let _ = fs::remove_file(&self.rpc_address_file); + } +} diff --git a/mullvad-tests/tests/connection.rs b/mullvad-tests/tests/connection.rs new file mode 100644 index 0000000000..38dba38c2d --- /dev/null +++ b/mullvad-tests/tests/connection.rs @@ -0,0 +1,76 @@ +#![cfg(all(target_os = "linux", feature = "integration-tests"))] + +extern crate mullvad_ipc_client; +extern crate mullvad_tests; +extern crate mullvad_types; + +use std::fs; +use std::sync::mpsc; +use std::time::Duration; + +use mullvad_ipc_client::DaemonRpcClient; +use mullvad_tests::{wait_for_file_write_finish, DaemonRunner}; +use mullvad_types::states::{DaemonState, SecurityState, TargetState}; + +const CONNECTING_STATE: DaemonState = DaemonState { + state: SecurityState::Unsecured, + target_state: TargetState::Secured, +}; + +#[test] +fn spawns_openvpn() { + let mut daemon = DaemonRunner::spawn(); + let mut rpc_client = daemon.rpc_client().unwrap(); + let openvpn_args_file = daemon.mock_openvpn_args_file(); + + assert!(!openvpn_args_file.exists()); + + rpc_client.set_account(Some("123456".to_owned())).unwrap(); + rpc_client.connect().unwrap(); + + wait_for_file_write_finish(&openvpn_args_file, Duration::from_secs(5)); + + assert!(openvpn_args_file.exists()); +} + +#[test] +fn respawns_openvpn_if_it_crashes() { + let mut daemon = DaemonRunner::spawn(); + let mut rpc_client = daemon.rpc_client().unwrap(); + let openvpn_args_file = daemon.mock_openvpn_args_file(); + + assert!(!openvpn_args_file.exists()); + + rpc_client.set_account(Some("123456".to_owned())).unwrap(); + rpc_client.connect().unwrap(); + + wait_for_file_write_finish(&openvpn_args_file, Duration::from_secs(5)); + + // Stop OpenVPN by removing the mock OpenVPN arguments file + fs::remove_file(&openvpn_args_file).expect("Failed to remove the mock OpenVPN arguments file"); + + wait_for_file_write_finish(&openvpn_args_file, Duration::from_secs(5)); + + assert!(openvpn_args_file.exists()); +} + +#[test] +fn changes_to_connecting_state() { + let mut daemon = DaemonRunner::spawn(); + let mut rpc_client = daemon.rpc_client().unwrap(); + let state_events = rpc_client.new_state_subscribe().unwrap(); + + rpc_client.set_account(Some("123456".to_owned())).unwrap(); + rpc_client.connect().unwrap(); + + assert_state_event(state_events, CONNECTING_STATE); + assert_eq!(rpc_client.get_state().unwrap(), CONNECTING_STATE); +} + +fn assert_state_event(receiver: mpsc::Receiver<DaemonState>, expected_state: DaemonState) { + let received_state = receiver + .recv_timeout(Duration::from_secs(1)) + .expect("Failed to receive new state event from daemon"); + + assert_eq!(received_state, expected_state); +} diff --git a/mullvad-daemon/tests/startup.rs b/mullvad-tests/tests/startup.rs index f273ee2ca3..379755536a 100644 --- a/mullvad-daemon/tests/startup.rs +++ b/mullvad-tests/tests/startup.rs @@ -1,22 +1,18 @@ -#[macro_use] -extern crate duct; -extern crate mullvad_ipc_client; -extern crate mullvad_paths; -extern crate os_pipe; -extern crate talpid_ipc; +#![cfg(all(target_os = "linux", feature = "integration-tests"))] -mod common; +extern crate mullvad_paths; +extern crate mullvad_tests; +extern crate mullvad_types; use std::fs::{self, Metadata}; use std::io; use std::time::Duration; -use common::DaemonRunner; +use mullvad_tests::DaemonRunner; +use mullvad_types::states::{DaemonState, SecurityState, TargetState}; use platform_specific::*; -// TODO: this test fails intermittently on Windows, would be nice to fix this later -#[cfg(not(windows))] #[test] fn rpc_info_file_permissions() { let rpc_file = mullvad_paths::get_rpc_address_path().unwrap(); @@ -29,17 +25,31 @@ fn rpc_info_file_permissions() { assert!(!rpc_file.exists()); - let mut daemon = DaemonRunner::spawn(); + let mut daemon = DaemonRunner::spawn_with_real_rpc_address_file(); daemon.assert_output("Wrote RPC connection info to", Duration::from_secs(10)); assert!(rpc_file.exists()); ensure_only_admin_can_write( - fs::metadata(&rpc_file).expect("failed to read RPC address file metadata"), + fs::metadata(&rpc_file).expect("Failed to read RPC address file metadata"), ); } +#[test] +fn starts_in_not_connected_state() { + let mut daemon = DaemonRunner::spawn(); + let mut rpc_client = daemon.rpc_client().expect("Failed to create RPC client"); + + let state = rpc_client.get_state().expect("Failed to read daemon state"); + let not_connected_state = DaemonState { + state: SecurityState::Unsecured, + target_state: TargetState::Unsecured, + }; + + assert_eq!(state, not_connected_state); +} + #[cfg(unix)] mod platform_specific { extern crate libc; |
