Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ae97188
Stop reconnecting and show a distinct state when there is no network
pappz Aug 10, 2026
fe55405
Cut Go service connections on network change instead of restarting
pappz Aug 10, 2026
9d508b5
Translate the no-network status into every supported locale
pappz Aug 10, 2026
dbec782
Bump the netbird submodule to the network sweep fixes
pappz Aug 12, 2026
a74e018
Bump the netbird submodule to the netsweep id type and test poller fixes
pappz Aug 12, 2026
e8953e5
Bump the netbird submodule to the nil connection listener fix
pappz Aug 12, 2026
1abe225
Bump the netbird submodule to the notifier publication fix
pappz Aug 12, 2026
b2c79fc
Bump the netbird submodule to the main merge
pappz Aug 12, 2026
b762df9
Bump the netbird submodule to the grpc client lint fix
pappz Aug 13, 2026
b75c5aa
Show the connection state on the persistent notification
pappz Aug 13, 2026
70af46b
Bump the netbird submodule to the stale-connection sweep
pappz Aug 13, 2026
281f802
Reduce NetworkSwitchNotifier to a forwarder
pappz Aug 13, 2026
a8f1099
Bump the netbird submodule to the prompt management and signal reconnect
pappz Aug 13, 2026
43b8fcc
Bump submodul version
pappz Aug 17, 2026
67fb6ba
Merge branch 'ux/ios-style-redesign' into android-airplane
pappz Aug 17, 2026
e9b53a9
Log the snapshot version name
pappz Aug 17, 2026
0422c83
Show the day in the session expiry notification
pappz Aug 18, 2026
1c81852
Bump submodule
pappz Aug 18, 2026
f02ee5c
Merge branch 'ux/ios-style-redesign' into android-airplane
pappz Aug 18, 2026
94ea1b6
Bump netbird version
pappz Aug 19, 2026
c432976
Add network transition e2e tests
pappz Aug 19, 2026
d581933
Align exit-node transition budgets with the ping-based tests
pappz Aug 19, 2026
f3eeedc
Share two suite-wide profiles instead of one per test case
pappz Aug 19, 2026
6fee41b
Revert "Share two suite-wide profiles instead of one per test case"
pappz Aug 19, 2026
65227a6
Run network transition tests with force relay ON
pappz Aug 19, 2026
8d1c7b2
Add NetworkTransitionSuite for group-selective CI runs
pappz Aug 19, 2026
12bc8ba
Document instrumented test groups in tests.md
pappz Aug 19, 2026
46c4832
Bump the netbird submodule to the network loss connection sweep
pappz Aug 19, 2026
e998a55
Bump the netbird submodule to the gRPC channel backoff reset
pappz Aug 19, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/build-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ jobs:
run: |
BASE_TAG=$(git describe --tags --abbrev=0)
SHORT_GIT_SHA=$(git rev-parse --short HEAD)
echo "version_name=${BASE_TAG}-snapshot-${SHORT_GIT_SHA}" >> "$GITHUB_OUTPUT"
VERSION_NAME="${BASE_TAG}-snapshot-${SHORT_GIT_SHA}"
echo "Version name: $VERSION_NAME"
echo "version_name=$VERSION_NAME" >> "$GITHUB_OUTPUT"

# Must stay identical to the Compute version code step in the sibling
# workflow. build-release.yml and build-snapshot.yml feed the same Play
Expand Down
5 changes: 5 additions & 0 deletions app/src/androidTest/java/io/netbird/client/e2e/E2eSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
PortAclTest.class,
DnsResolutionTest.class,
ExitNodeRouteTest.class,
ExitNodeNetworkTransitionTest.class,
// Last on purpose: its final case (B2, cellular->WiFi handover speed)
// is expected to fail until PR #243 merges, and the FailFast listener
// would skip everything scheduled after that failure.
NetworkTransitionTest.class,
})
public class E2eSuite {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package io.netbird.client.e2e;

import io.netbird.client.MainActivity;

import android.os.Bundle;
import android.util.Log;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
* Scenario C1 of the network transition matrix (see the table in
* {@link NetworkTransitionTest}): a client whose network map routes ALL
* traffic through an exit node must re-establish that egress after network
* transitions — the exit-node route is only usable once the peer connection
* to the exit node itself is back, so this proves real peer recovery, not
* just a Connected status.
*
* <p>Reuses the {@link ExitNodeRouteTest} profile ({@code exitNodeSetupKey},
* exit node egress IP {@code 3.121.38.77}) and its verification: a real HTTPS
* GET to {@code https://api.ipify.org} must report the exit node's public IP.
* Two transitions are exercised on the emulator's virtual transports:
* <ol>
* <li>WiFi loss -> cellular fallback (B1-style) — egress must return
* through the exit node within {@link #SWITCH_RECOVERY_SEC};</li>
* <li>full blackout and restore (A3-style) — the UI must report
* "No network available" while dark, and egress must return through
* the exit node within {@link #BLACKOUT_RECOVERY_SEC}.</li>
* </ol>
*
* <p>The budgets match the ping-based ones in {@link NetworkTransitionTest}:
* even though each probe is a full HTTPS request through the exit node, the
* probes run with a short timeout and tight polling so a request hung on a
* dead route cannot blur the measurement.
*/
@RunWith(AndroidJUnit4.class)
public class ExitNodeNetworkTransitionTest {

private static final String TAG = "NBExitNodeNetTest";

private static final String EGRESS_CHECK_URL = "https://api.ipify.org";
private static final String EXIT_NODE_PUBLIC_IP = "3.121.38.77";
private static final String STATUS_NO_NETWORK = "No network available";

private static final long CONNECT_TIMEOUT_SEC = 20;
/** Budget for the initial egress check — setup, not an assertion of speed. */
private static final long BASELINE_EGRESS_TIMEOUT_SEC = 90;
private static final long NO_NETWORK_UI_TIMEOUT_SEC = 15;
private static final long SWITCH_RECOVERY_SEC = 5;
private static final long BLACKOUT_RECOVERY_SEC = 15;
/** Short per-probe timeout + tight polling so a hung request cannot blur the measurement. */
private static final int PROBE_TIMEOUT_MS = 2_000;
private static final long PROBE_POLL_MS = 1_000;

private VpnTestHarness harness;
private String profileName;

@Before
public void skipIfPreviousFailed() {
FailFast.skipIfAborted();
}

@After
public void tearDown() throws Exception {
if (harness != null) {
harness.setWifi(true);
harness.setMobileData(true);
harness.disableTouchVisualization();
}
if (profileName != null && harness != null) {
LoginFlow.removeProfile(E2eAppRule.activity(), harness.device(), profileName);
}
}

@Test
public void egressSurvivesNetworkTransitions() throws Exception {
Bundle args = InstrumentationRegistry.getArguments();
String setupKey = args.getString("exitNodeSetupKey");
assertNotNull("exitNodeSetupKey instrumentation argument is required", setupKey);
assertTrue("exitNodeSetupKey must not be blank", !setupKey.trim().isEmpty());

MainActivity activity = E2eAppRule.activity();
harness = new VpnTestHarness(activity);
harness.enableTouchVisualization();
harness.grantVpnConsent();
harness.setWifi(true);
harness.setMobileData(true);

// Force relay ON for the network transition tests: they measure the
// relay path's failover; the P2P/ICE failover path stays out of scope
// until it is optimized (a stale ICE connection blocks the switch to
// the ready relay connection for ~7s). See NetworkTransitionTest.
LoginFlow.setForceRelay(activity, harness.device(), true);

profileName = LoginFlow.createProfileAndLogin(
activity, harness.device(), "exit-node-network", setupKey);

boolean connected = harness.connectAndAwait(CONNECT_TIMEOUT_SEC);
if (!connected) {
LoginFlow.dumpScreenshot(harness.device(), "exit-node-net-connect-timeout");
}
assertTrue("VPN did not reach connected state within " + CONNECT_TIMEOUT_SEC + "s",
connected);

assertEgressViaExitNode("baseline", BASELINE_EGRESS_TIMEOUT_SEC);

harness.setWifi(false);
assertEgressViaExitNode("after WiFi loss (cellular fallback)", SWITCH_RECOVERY_SEC);
harness.setWifi(true);

harness.setWifi(false);
harness.setMobileData(false);
assertTrue("status must show '" + STATUS_NO_NETWORK + "' within "
+ NO_NETWORK_UI_TIMEOUT_SEC + "s of losing all transports",
harness.awaitStatusText(STATUS_NO_NETWORK, NO_NETWORK_UI_TIMEOUT_SEC));

harness.setWifi(true);
harness.setMobileData(true);
assertEgressViaExitNode("after blackout restore", BLACKOUT_RECOVERY_SEC);
}

private void assertEgressViaExitNode(String phase, long budgetSec) throws Exception {
long start = System.currentTimeMillis();
boolean viaExitNode = harness.waitForHttpBodyContains(
EGRESS_CHECK_URL, EXIT_NODE_PUBLIC_IP, budgetSec, PROBE_TIMEOUT_MS, PROBE_POLL_MS);
long elapsedSec = (System.currentTimeMillis() - start + 999) / 1000;
Log.i(TAG, "C1 " + phase + ": exit-node egress "
+ (viaExitNode ? "verified in " + elapsedSec + "s" : "NOT verified within " + budgetSec + "s"));
if (!viaExitNode) {
LoginFlow.dumpScreenshot(harness.device(), "exit-node-egress-lost");
}
assertTrue("C1 " + phase + ": egress IP from " + EGRESS_CHECK_URL + " was not the exit "
+ "node's " + EXIT_NODE_PUBLIC_IP + " within " + budgetSec + "s — slow recovery "
+ "means the fallback logic reconnected, not the network-change fast path",
viaExitNode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.netbird.client.e2e;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

/**
* Focused group: only the network transition scenarios (the matrix in
* {@link NetworkTransitionTest} plus the exit-node variant), for CI runs that
* iterate on connection switching without paying for the full {@link E2eSuite}.
* The mobile-e2e workflow selects it via its suite dropdown, which maps to
* <pre>
* -Pandroid.testInstrumentationRunnerArguments.class=io.netbird.client.e2e.NetworkTransitionSuite
* </pre>
*
* <p>No suite-level setup is needed: both classes configure what they depend
* on themselves (force relay ON, transports restored), so the group runs the
* same standalone as inside the full suite. Order matters and mirrors
* {@link E2eSuite}: {@link NetworkTransitionTest} goes last because its final
* case is expected to fail until PR #243 merges, and the FailFast listener
* would skip everything scheduled after that failure.
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({
ExitNodeNetworkTransitionTest.class,
NetworkTransitionTest.class,
})
public class NetworkTransitionSuite {
}
Loading