Skip to content

Add test that makes sure connecting works without the API #6013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 23 additions & 34 deletions test/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ prost = "0.12.0"
prost-types = "0.12.0"
tarpc = { version = "0.30", features = ["tokio1", "serde-transport", "serde1"] }
# Logging
env_logger = "0.10.0"
env_logger = "0.11.0"
thiserror = "1.0.57"
log = "0.4"
colored = "2.0.0"
Expand Down
30 changes: 2 additions & 28 deletions test/test-manager/src/tests/account.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::tests::helpers::{login_with_retries, THROTTLE_RETRY_DELAY};

use super::config::TEST_CONFIG;
use super::{helpers, ui, Error, TestContext};
use mullvad_api::DevicesProxy;
Expand All @@ -10,8 +12,6 @@ use talpid_types::net::wireguard;
use test_macro::test_function;
use test_rpc::ServiceClient;

const THROTTLE_RETRY_DELAY: Duration = Duration::from_secs(120);

/// Log in and create a new device for the account.
#[test_function(always_run = true, must_succeed = true, priority = -100)]
pub async fn test_login(
Expand Down Expand Up @@ -241,32 +241,6 @@ pub fn new_device_client() -> DevicesProxy {
DevicesProxy::new(rest_handle)
}

/// Log in and retry if it fails due to throttling
pub async fn login_with_retries(
mullvad_client: &mut MullvadProxyClient,
) -> Result<(), mullvad_management_interface::Error> {
loop {
match mullvad_client
.login_account(TEST_CONFIG.account_number.clone())
.await
{
Err(mullvad_management_interface::Error::Rpc(status))
if status.message().to_uppercase().contains("THROTTLED") =>
{
// Work around throttling errors by sleeping
log::debug!(
"Login failed due to throttling. Sleeping for {} seconds",
THROTTLE_RETRY_DELAY.as_secs()
);

tokio::time::sleep(THROTTLE_RETRY_DELAY).await;
}
Err(err) => break Err(err),
Ok(_) => break Ok(()),
}
}
}

pub async fn list_devices_with_retries(
device_client: &DevicesProxy,
) -> Result<Vec<Device>, mullvad_api::rest::Error> {
Expand Down
87 changes: 70 additions & 17 deletions test/test-manager/src/tests/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{config::TEST_CONFIG, Error, WAIT_FOR_TUNNEL_STATE_TIMEOUT};
use super::{config::TEST_CONFIG, Error, TestContext, WAIT_FOR_TUNNEL_STATE_TIMEOUT};
use crate::network_monitor::{
self, start_packet_monitor, MonitorOptions, MonitorUnexpectedlyStopped, PacketMonitor,
};
Expand All @@ -22,6 +22,8 @@ use std::{
use talpid_types::net::wireguard::{PeerConfig, PrivateKey, TunnelConfig};
use test_rpc::{package::Package, AmIMullvad, ServiceClient};

pub const THROTTLE_RETRY_DELAY: Duration = Duration::from_secs(120);

#[macro_export]
macro_rules! assert_tunnel_state {
($mullvad_client:expr, $pattern:pat) => {{
Expand Down Expand Up @@ -199,19 +201,44 @@ pub async fn ping_sized_with_timeout(
.map_err(Error::Rpc)
}

/// Log in and retry if it fails due to throttling
pub async fn login_with_retries(
mullvad_client: &mut MullvadProxyClient,
) -> Result<(), mullvad_management_interface::Error> {
loop {
match mullvad_client
.login_account(TEST_CONFIG.account_number.clone())
.await
{
Err(mullvad_management_interface::Error::Rpc(status))
if status.message().to_uppercase().contains("THROTTLED") =>
{
// Work around throttling errors by sleeping
log::debug!(
"Login failed due to throttling. Sleeping for {} seconds",
THROTTLE_RETRY_DELAY.as_secs()
);

tokio::time::sleep(THROTTLE_RETRY_DELAY).await;
}
Err(err) => break Err(err),
Ok(_) => break Ok(()),
}
}
}

/// Try to connect to a Mullvad Tunnel.
///
/// If that fails to begin to connect, [`Error::DaemonError`] is returned. If it fails to connect
/// after that, the daemon ends up in the [`TunnelState::Error`] state, and
/// [`Error::UnexpectedErrorState`] is returned.
/// # Returns
/// - `Result::Ok` if the daemon successfully connected to a tunnel
/// - `Result::Err` if:
/// - The daemon failed to even begin connecting. Then [`Error::Rpc`] is returned.
/// - The daemon started to connect but ended up in the [`TunnelState::Error`] state.
/// Then [`Error::UnexpectedErrorState`] is returned
pub async fn connect_and_wait(mullvad_client: &mut MullvadProxyClient) -> Result<(), Error> {
log::info!("Connecting");

mullvad_client
.connect_tunnel()
.await
.map_err(|error| Error::Daemon(format!("failed to begin connecting: {}", error)))?;

mullvad_client.connect_tunnel().await?;
let new_state = wait_for_tunnel_state(mullvad_client.clone(), |state| {
matches!(
state,
Expand All @@ -231,11 +258,8 @@ pub async fn connect_and_wait(mullvad_client: &mut MullvadProxyClient) -> Result

pub async fn disconnect_and_wait(mullvad_client: &mut MullvadProxyClient) -> Result<(), Error> {
log::info!("Disconnecting");
mullvad_client.disconnect_tunnel().await?;

mullvad_client
.disconnect_tunnel()
.await
.map_err(|error| Error::Daemon(format!("failed to begin disconnecting: {}", error)))?;
wait_for_tunnel_state(mullvad_client.clone(), |state| {
matches!(state, TunnelState::Disconnected { .. })
})
Expand Down Expand Up @@ -308,6 +332,30 @@ where
}
}

/// Set environment variables specified by `env` and restart the Mullvad daemon.
/// Returns a new [rpc client][`MullvadProxyClient`], since the old client *probably*
/// can't communicate with the new daemon.
///
/// # Note
/// This is just a thin wrapper around [`ServiceClient::set_daemon_environment`] which also
/// invalidates the old [`MullvadProxyClient`].
pub async fn restart_daemon_with<K, V, Env>(
rpc: &ServiceClient,
test_context: &TestContext,
_: MullvadProxyClient, // Just consume the old proxy client
env: Env,
) -> Result<MullvadProxyClient, Error>
where
Env: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
rpc.set_daemon_environment(env).await?;
// Need to create a new `mullvad_client` here after the restart
// otherwise we *probably* can't communicate with the daemon.
Ok(test_context.rpc_provider.new_client().await)
}

pub async fn geoip_lookup_with_retries(rpc: &ServiceClient) -> Result<AmIMullvad, Error> {
const MAX_ATTEMPTS: usize = 5;
const BEFORE_RETRY_DELAY: Duration = Duration::from_secs(2);
Expand Down Expand Up @@ -409,6 +457,11 @@ pub fn unreachable_wireguard_tunnel() -> talpid_types::net::wireguard::Connectio
}
}

/// Return the current `MULLVAD_API_HOST` et al.
///
/// # Note
/// This is independent of the running daemon's environment.
/// It is solely dependant on the current value of [`TEST_CONFIG`].
pub fn get_app_env() -> HashMap<String, String> {
use mullvad_api::env;
use std::net::ToSocketAddrs;
Expand All @@ -420,10 +473,10 @@ pub fn get_app_env() -> HashMap<String, String> {
.next()
.unwrap();

let api_host_env = (env::API_HOST_VAR.to_string(), api_host);
let api_addr_env = (env::API_ADDR_VAR.to_string(), api_addr.to_string());

[api_host_env, api_addr_env].into_iter().collect()
HashMap::from_iter(vec![
(env::API_HOST_VAR.to_string(), api_host),
(env::API_ADDR_VAR.to_string(), api_addr.to_string()),
])
}

/// Return a filtered version of the daemon's relay list.
Expand Down
32 changes: 32 additions & 0 deletions test/test-manager/src/tests/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::helpers::{
};
use super::{config::TEST_CONFIG, Error, TestContext};
use crate::network_monitor::{start_packet_monitor, MonitorOptions};
use crate::tests::helpers::login_with_retries;

use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::relay_constraints::{
Expand Down Expand Up @@ -770,3 +771,34 @@ pub async fn test_local_socks_bridge(

Ok(())
}

/// Verify that the app can connect to a VPN server and get working internet when the API is down.
/// As long as the user has managed to log in to the app, establishing a tunnel should work even if the API is down (This includes actually being down, not just censored).
///
/// The test procedure is as follows:
/// 1. The app is logged in
/// 2. The app is killed
/// 3. The API is "removed" (override API IP/host to something bogus)
/// 4. The app is restarted
/// 5. Verify that it starts as intended and a tunnel can be established
#[test_function]
pub async fn test_establish_tunnel_without_api(
ctx: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
// 1
login_with_retries(&mut mullvad_client).await?;
// 2
rpc.stop_mullvad_daemon().await?;
// 3
let borked_env = [("MULLVAD_API_ADDR", "1.3.3.7:421")];
// 4
log::debug!("Restarting the daemon with the following overrides: {borked_env:?}");
let mut mullvad_client =
helpers::restart_daemon_with(&rpc, &ctx, mullvad_client, borked_env).await?;
// 5
connect_and_wait(&mut mullvad_client).await?;
// Profit
Ok(())
}
5 changes: 5 additions & 0 deletions test/test-rpc/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ impl ServiceClient {
Ok(())
}

/// Set environment variables specified by `env` and restart the Mullvad daemon.
///
/// # Returns
/// - `Result::Ok` if the daemon was successfully restarted.
/// - `Result::Err(Error)` if the daemon could not be restarted and is thus no longer running.
pub async fn set_daemon_environment<Env, K, V>(&self, env: Env) -> Result<(), Error>
where
Env: IntoIterator<Item = (K, V)>,
Expand Down