summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2018-06-20 08:10:30 -0300
committerJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2018-06-20 08:10:30 -0300
commit3e7a83f27a10cf6630ba5ff885b64cc24b02e9e3 (patch)
tree674ed8c397d5cfb0ba847fd562376f78ca5cbcf6
parent33f015b5c94c9900db5f9e7f7296f57e759094c5 (diff)
parente92c41b73121d491899f63e480505959a7d15f99 (diff)
downloadmullvadvpn-3e7a83f27a10cf6630ba5ff885b64cc24b02e9e3.tar.xz
mullvadvpn-3e7a83f27a10cf6630ba5ff885b64cc24b02e9e3.zip
Merge branch 'mock-openvpn'
-rw-r--r--.travis.yml8
-rw-r--r--Cargo.lock17
-rw-r--r--Cargo.toml1
-rw-r--r--README.md7
-rwxr-xr-xintegration-tests.sh13
-rw-r--r--mullvad-daemon/Cargo.toml5
-rw-r--r--mullvad-daemon/tests/common/mod.rs131
-rw-r--r--mullvad-ipc-client/src/lib.rs63
-rw-r--r--mullvad-tests/Cargo.toml21
-rw-r--r--mullvad-tests/src/bin/mock_openvpn.rs58
-rw-r--r--mullvad-tests/src/lib.rs273
-rw-r--r--mullvad-tests/tests/connection.rs76
-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",
diff --git a/README.md b/README.md
index becd35065f..423c6a0f52 100644
--- a/README.md
+++ b/README.md
@@ -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;