diff --git a/firebase-database/CHANGELOG.md b/firebase-database/CHANGELOG.md
index 7918e6fa2a1..884bd96b5df 100644
--- a/firebase-database/CHANGELOG.md
+++ b/firebase-database/CHANGELOG.md
@@ -1,5 +1,9 @@
# Unreleased
+- [fixed] Fixed a crash when the Realtime Database server sent a host or
+ session ID that wasn't valid in a URL. The SDK now ignores the value.
+ (#8780)
+
# 23.0.0
- [changed] **Breaking Change**: Updated minSdkVersion to API level 24 or higher. (#8638)
diff --git a/firebase-database/src/main/java/com/google/firebase/database/connection/Connection.java b/firebase-database/src/main/java/com/google/firebase/database/connection/Connection.java
index 1ec17c5c4b8..7d53ad4492e 100644
--- a/firebase-database/src/main/java/com/google/firebase/database/connection/Connection.java
+++ b/firebase-database/src/main/java/com/google/firebase/database/connection/Connection.java
@@ -221,6 +221,15 @@ private void onConnectionReady(long timestamp, String sessionId) {
}
private void onReset(String host) {
+ if (host != null && !hostInfo.isUsableHost(host)) {
+ // Ignore the host and keep the current one. The server is likely to reset the next
+ // connection the same way, so close with OTHER instead of SERVER_RESET: the persistent
+ // connection then reconnects with its usual backoff instead of immediately, which avoids a
+ // tight reconnect loop.
+ logger.warn("Got a reset with a host that can't be used in a URL, ignoring the host");
+ close(DisconnectReason.OTHER);
+ return;
+ }
if (logger.logsDebug())
logger.debug(
"Got a reset; killing connection to "
diff --git a/firebase-database/src/main/java/com/google/firebase/database/connection/HostInfo.java b/firebase-database/src/main/java/com/google/firebase/database/connection/HostInfo.java
index 156a73508f3..dd65d97fb5b 100644
--- a/firebase-database/src/main/java/com/google/firebase/database/connection/HostInfo.java
+++ b/firebase-database/src/main/java/com/google/firebase/database/connection/HostInfo.java
@@ -14,6 +14,8 @@
package com.google.firebase.database.connection;
+import androidx.annotation.Nullable;
+import com.google.firebase.database.logging.LogWrapper;
import java.net.URI;
public class HostInfo {
@@ -55,6 +57,60 @@ public static URI getConnectionUrl(
return URI.create(url);
}
+ /**
+ * Returns the URL for a new connection to this database.
+ *
+ *
The cached host and the last session ID were sent by the server. If one of them can't be
+ * used in the URL, it is ignored with a warning: the connection goes to the configured host, or
+ * starts without a last session ID. Each value is checked on its own, so that a bad session ID
+ * doesn't also discard a good host. Otherwise, if the configured host reset the connection to
+ * that host, the client would reconnect in a tight loop.
+ */
+ URI getConnectionUrl(
+ @Nullable String optCachedHost, @Nullable String optLastSessionId, LogWrapper logger) {
+ String connectionHost = host;
+ if (optCachedHost != null) {
+ if (isUsableHost(optCachedHost)) {
+ connectionHost = optCachedHost;
+ } else {
+ logger.warn(
+ "Ignoring a host from the server that can't be used in a URL, connecting to the"
+ + " configured host instead");
+ }
+ }
+ if (optLastSessionId != null) {
+ URI url = tryGetConnectionUrl(connectionHost, optLastSessionId);
+ if (url != null) {
+ return url;
+ }
+ logger.warn("Ignoring a session ID from the server that can't be used in a URL");
+ }
+ return getConnectionUrl(connectionHost, secure, namespace, /* optLastSessionId= */ null);
+ }
+
+ /**
+ * Returns whether a host that the server sent can be used to connect to this database, that is,
+ * whether the connection URL for it is a valid URI that names a host. This only checks the
+ * syntax. It doesn't restrict which hosts the server can send.
+ */
+ boolean isUsableHost(String host) {
+ URI url = tryGetConnectionUrl(host, /* optLastSessionId= */ null);
+ // Without a host in the URL, the websocket would connect to the loopback address.
+ return url != null && url.getHost() != null;
+ }
+
+ /** Returns the connection URL, or null if it isn't a valid URI. */
+ @Nullable
+ private URI tryGetConnectionUrl(String host, @Nullable String optLastSessionId) {
+ try {
+ return getConnectionUrl(host, secure, namespace, optLastSessionId);
+ } catch (IllegalArgumentException e) {
+ // URI.create throws if the URL contains characters that aren't allowed in a URI, such as a
+ // space.
+ return null;
+ }
+ }
+
public String getHost() {
return this.host;
}
diff --git a/firebase-database/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java b/firebase-database/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
index 5d1223d9968..73026cd5b14 100644
--- a/firebase-database/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
+++ b/firebase-database/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
@@ -181,10 +181,7 @@ public WebsocketConnection(
private WSClient createConnection(
HostInfo hostInfo, String optCachedHost, String appCheckToken, String optLastSessionId) {
- String host = (optCachedHost != null) ? optCachedHost : hostInfo.getHost();
- URI uri =
- HostInfo.getConnectionUrl(
- host, hostInfo.isSecure(), hostInfo.getNamespace(), optLastSessionId);
+ URI uri = hostInfo.getConnectionUrl(optCachedHost, optLastSessionId, logger);
Map extraHeaders = new HashMap();
extraHeaders.put("User-Agent", connectionContext.getUserAgent());
extraHeaders.put("X-Firebase-GMPID", connectionContext.getApplicationId());
diff --git a/firebase-database/src/test/java/com/google/firebase/database/connection/ConnectionResetTest.java b/firebase-database/src/test/java/com/google/firebase/database/connection/ConnectionResetTest.java
new file mode 100644
index 00000000000..39f9b219cf0
--- /dev/null
+++ b/firebase-database/src/test/java/com/google/firebase/database/connection/ConnectionResetTest.java
@@ -0,0 +1,161 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.firebase.database.connection;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.firebase.database.MapBuilder;
+import com.google.firebase.database.logging.Logger;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedConstruction;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests how a connection handles a reset control message, which tells it to use another host. */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class ConnectionResetTest {
+
+ /** Hosts that the server or the emulator can send. */
+ private static final List VALID_HOSTS =
+ Arrays.asList(
+ "s-usc1b-nss-2073.firebaseio.com",
+ "x.europe-west1.firebasedatabase.app",
+ "localhost:9000",
+ "10.0.2.2:9000",
+ "[::1]:9000");
+
+ /** Hosts that can't be used in a connection URL. */
+ private static final List INVALID_HOSTS =
+ Arrays.asList("bad host", "a|b.firebaseio.com", "a^b.firebaseio.com", "");
+
+ private final Logger logger = mock(Logger.class);
+ private MockedConstruction websockets;
+ private ConnectionContext context;
+
+ @Before
+ public void setUp() {
+ // Log everything, so that the debug logging code paths run too.
+ when(logger.getLogLevel()).thenReturn(Logger.Level.DEBUG);
+ // Replace the websocket with a mock, so that the tests can check that it is closed. The
+ // connection is never opened, and server messages are delivered by calling onMessage directly.
+ websockets = mockConstruction(WebsocketConnection.class);
+ context =
+ new ConnectionContext(
+ logger,
+ mock(ConnectionTokenProvider.class),
+ mock(ConnectionTokenProvider.class),
+ mock(ScheduledExecutorService.class),
+ /* persistenceEnabled= */ false,
+ "test-sdk-version",
+ "test-user-agent",
+ "test-app-id",
+ /* sslCacheDirectory= */ null);
+ }
+
+ @After
+ public void tearDown() {
+ websockets.close();
+ }
+
+ @Test
+ public void resetWithValidHost_cachesHostAndReconnectsImmediately() {
+ for (String host : VALID_HOSTS) {
+ Connection.Delegate delegate = sendResetToNewConnection(host);
+
+ verify(delegate).onCacheHost(host);
+ verify(delegate).onDisconnect(Connection.DisconnectReason.SERVER_RESET);
+ verify(lastWebsocket()).close();
+ }
+ }
+
+ @Test
+ public void resetWithoutHost_clearsCachedHostAndReconnectsImmediately() {
+ Connection.Delegate delegate = sendResetToNewConnection(/* host= */ null);
+
+ verify(delegate).onCacheHost(null);
+ verify(delegate).onDisconnect(Connection.DisconnectReason.SERVER_RESET);
+ verify(lastWebsocket()).close();
+ }
+
+ @Test
+ public void resetWithHostThatIsNotValidInUrl_keepsCachedHostAndReconnectsWithBackoff() {
+ for (String host : INVALID_HOSTS) {
+ Connection.Delegate delegate = sendResetToNewConnection(host);
+
+ // The persistent connection keeps the host it had. OTHER, unlike SERVER_RESET, makes it
+ // reconnect with its usual backoff, so a server that keeps sending this reset can't cause a
+ // tight reconnect loop.
+ verify(delegate, never()).onCacheHost(any());
+ verify(delegate).onDisconnect(Connection.DisconnectReason.OTHER);
+ verify(lastWebsocket()).close();
+ }
+ }
+
+ @Test
+ public void resetWithHostThatIsNotValidInUrl_logsWarningWithoutHost() {
+ sendResetToNewConnection("bad host");
+
+ ArgumentCaptor warnings = ArgumentCaptor.forClass(String.class);
+ verify(logger, atLeastOnce())
+ .onLogMessage(eq(Logger.Level.WARN), anyString(), warnings.capture(), anyLong());
+ for (String warning : warnings.getAllValues()) {
+ assertThat(warning).doesNotContain("bad host");
+ }
+ }
+
+ /** Sends a reset with {@code host} to a new connection, and returns the connection's delegate. */
+ private Connection.Delegate sendResetToNewConnection(String host) {
+ Connection.Delegate delegate = mock(Connection.Delegate.class);
+ Connection connection =
+ new Connection(
+ context,
+ new HostInfo("test.firebaseio.com", "test", /* secure= */ true),
+ /* cachedHost= */ null,
+ delegate,
+ /* optLastSessionId= */ null,
+ "test-app-check-token");
+ connection.onMessage(resetMessage(host));
+ return delegate;
+ }
+
+ private WebsocketConnection lastWebsocket() {
+ List constructed = websockets.constructed();
+ return constructed.get(constructed.size() - 1);
+ }
+
+ private static Map resetMessage(String host) {
+ Map reset = new MapBuilder().put("t", "r").put("d", host).build();
+ return new MapBuilder().put("t", "c").put("d", reset).build();
+ }
+}
diff --git a/firebase-database/src/test/java/com/google/firebase/database/connection/HostInfoTest.java b/firebase-database/src/test/java/com/google/firebase/database/connection/HostInfoTest.java
new file mode 100644
index 00000000000..eae40150d5b
--- /dev/null
+++ b/firebase-database/src/test/java/com/google/firebase/database/connection/HostInfoTest.java
@@ -0,0 +1,190 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.firebase.database.connection;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.firebase.database.logging.LogWrapper;
+import com.google.firebase.database.logging.Logger;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class HostInfoTest {
+
+ private static final String CONFIGURED_HOST_URL = "wss://test.firebaseio.com/.ws?ns=test&v=5";
+ private static final String SERVER_HOST = "s-usc1b-nss-2073.firebaseio.com";
+ private static final String SERVER_HOST_URL =
+ "wss://s-usc1b-nss-2073.firebaseio.com/.ws?ns=test&v=5";
+
+ /** Hosts that the server or the emulator can send. */
+ private static final List VALID_HOSTS =
+ Arrays.asList(
+ "s-usc1b-nss-2073.firebaseio.com",
+ "x.europe-west1.firebasedatabase.app",
+ "localhost:9000",
+ "10.0.2.2:9000",
+ "[::1]:9000");
+
+ /**
+ * Hosts that can't be used in a connection URL. With the first ones, the URL isn't a valid URI.
+ * With the last ones, it is, but it doesn't name a host.
+ */
+ private static final List INVALID_HOSTS =
+ Arrays.asList(
+ "bad host",
+ "a|b.firebaseio.com",
+ "a^b.firebaseio.com",
+ "{a}.firebaseio.com",
+ "a\"b.firebaseio.com",
+ "a\\b.firebaseio.com",
+ "a%zz.firebaseio.com",
+ "",
+ "test.firebaseio.com:port");
+
+ /** Session IDs that make the connection URL an invalid URI. */
+ private static final List INVALID_SESSION_IDS =
+ Arrays.asList("a b", "a|b", "a\"b", "{a}", "%");
+
+ private final Logger logger = mock(Logger.class);
+ private final LogWrapper logWrapper = new LogWrapper(logger, "HostInfoTest");
+ private final HostInfo hostInfo = new HostInfo("test.firebaseio.com", "test", /* secure= */ true);
+ private final HostInfo emulatorHostInfo =
+ new HostInfo("10.0.2.2:9000", "test", /* secure= */ false);
+
+ @Test
+ public void isUsableHost_acceptsValidHosts() {
+ for (String host : VALID_HOSTS) {
+ assertWithMessage("host: %s", host).that(hostInfo.isUsableHost(host)).isTrue();
+ assertWithMessage("emulator, host: %s", host)
+ .that(emulatorHostInfo.isUsableHost(host))
+ .isTrue();
+ }
+ }
+
+ @Test
+ public void isUsableHost_rejectsHostsThatCannotBeUsedInUrl() {
+ for (String host : INVALID_HOSTS) {
+ assertWithMessage("host: %s", host).that(hostInfo.isUsableHost(host)).isFalse();
+ }
+ }
+
+ @Test
+ public void getConnectionUrl_withoutHostOrSessionIdFromServer_usesConfiguredHost() {
+ assertThat(getConnectionUrl(/* cachedHost= */ null, /* lastSessionId= */ null))
+ .isEqualTo(CONFIGURED_HOST_URL);
+ assertNoWarnings();
+ }
+
+ @Test
+ public void getConnectionUrl_withValidHostAndSessionId_usesBoth() {
+ assertThat(getConnectionUrl(SERVER_HOST, "session-id"))
+ .isEqualTo(SERVER_HOST_URL + "&ls=session-id");
+ assertNoWarnings();
+ }
+
+ @Test
+ public void getConnectionUrl_withValidHosts_usesThem() {
+ for (String host : VALID_HOSTS) {
+ assertThat(getConnectionUrl(host, /* lastSessionId= */ null))
+ .isEqualTo("wss://" + host + "/.ws?ns=test&v=5");
+ }
+ assertNoWarnings();
+ }
+
+ @Test
+ public void getConnectionUrl_forEmulator_usesInsecureScheme() {
+ assertThat(emulatorHostInfo.getConnectionUrl(null, null, logWrapper).toString())
+ .isEqualTo("ws://10.0.2.2:9000/.ws?ns=test&v=5");
+ assertThat(
+ emulatorHostInfo
+ .getConnectionUrl("localhost:9000", "session-id", logWrapper)
+ .toString())
+ .isEqualTo("ws://localhost:9000/.ws?ns=test&v=5&ls=session-id");
+ assertNoWarnings();
+ }
+
+ @Test
+ public void getConnectionUrl_withInvalidHost_usesConfiguredHost() {
+ for (String host : INVALID_HOSTS) {
+ assertWithMessage("host: %s", host)
+ .that(getConnectionUrl(host, /* lastSessionId= */ null))
+ .isEqualTo(CONFIGURED_HOST_URL);
+ }
+ }
+
+ @Test
+ public void getConnectionUrl_withInvalidHost_keepsSessionId() {
+ assertThat(getConnectionUrl("bad host", "session-id"))
+ .isEqualTo(CONFIGURED_HOST_URL + "&ls=session-id");
+ }
+
+ @Test
+ public void getConnectionUrl_withInvalidSessionId_omitsSessionId() {
+ for (String sessionId : INVALID_SESSION_IDS) {
+ assertWithMessage("session ID: %s", sessionId)
+ .that(getConnectionUrl(/* cachedHost= */ null, sessionId))
+ .isEqualTo(CONFIGURED_HOST_URL);
+ }
+ }
+
+ @Test
+ public void getConnectionUrl_withInvalidSessionId_keepsHost() {
+ // If this used the configured host instead, and the server there reset the connection to the
+ // same host again, the client would reconnect in a tight loop.
+ assertThat(getConnectionUrl(SERVER_HOST, "a b")).isEqualTo(SERVER_HOST_URL);
+ }
+
+ @Test
+ public void getConnectionUrl_withInvalidHostAndSessionId_usesConfiguredHostWithoutSessionId() {
+ assertThat(getConnectionUrl("a|b.firebaseio.com", "a b")).isEqualTo(CONFIGURED_HOST_URL);
+ }
+
+ @Test
+ public void getConnectionUrl_withInvalidValues_logsWarningsWithoutTheValues() {
+ getConnectionUrl("bad host", "bad session");
+
+ ArgumentCaptor warnings = ArgumentCaptor.forClass(String.class);
+ verify(logger, times(2))
+ .onLogMessage(eq(Logger.Level.WARN), anyString(), warnings.capture(), anyLong());
+ for (String warning : warnings.getAllValues()) {
+ assertThat(warning).doesNotContain("bad host");
+ assertThat(warning).doesNotContain("bad session");
+ }
+ }
+
+ private String getConnectionUrl(String cachedHost, String lastSessionId) {
+ return hostInfo.getConnectionUrl(cachedHost, lastSessionId, logWrapper).toString();
+ }
+
+ private void assertNoWarnings() {
+ verify(logger, never())
+ .onLogMessage(eq(Logger.Level.WARN), anyString(), anyString(), anyLong());
+ }
+}
diff --git a/firebase-database/src/test/java/com/google/firebase/database/connection/WebsocketConnectionTest.java b/firebase-database/src/test/java/com/google/firebase/database/connection/WebsocketConnectionTest.java
new file mode 100644
index 00000000000..50dab141748
--- /dev/null
+++ b/firebase-database/src/test/java/com/google/firebase/database/connection/WebsocketConnectionTest.java
@@ -0,0 +1,123 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.firebase.database.connection;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+
+import com.google.firebase.database.logging.Logger;
+import com.google.firebase.database.tubesock.WebSocket;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockedConstruction;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/**
+ * Tests the URL that a new websocket connection uses. The persistent connection creates a new
+ * websocket connection every time it reconnects, with the host and the session ID that the server
+ * sent on an earlier connection.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class WebsocketConnectionTest {
+
+ private static final HostInfo HOST_INFO =
+ new HostInfo("test.firebaseio.com", "test", /* secure= */ true);
+ private static final String SERVER_HOST = "s-usc1b-nss-2073.firebaseio.com";
+
+ private final List urls = new ArrayList<>();
+ private MockedConstruction webSockets;
+ private ConnectionContext context;
+
+ @Before
+ public void setUp() {
+ // Record the URL of each websocket instead of creating a real one. The tests never call
+ // open(), so nothing connects to the network.
+ webSockets =
+ mockConstruction(
+ WebSocket.class,
+ (webSocket, construction) -> urls.add((URI) construction.arguments().get(1)));
+ context =
+ new ConnectionContext(
+ mock(Logger.class),
+ mock(ConnectionTokenProvider.class),
+ mock(ConnectionTokenProvider.class),
+ mock(ScheduledExecutorService.class),
+ /* persistenceEnabled= */ false,
+ "test-sdk-version",
+ "test-user-agent",
+ "test-app-id",
+ /* sslCacheDirectory= */ null);
+ }
+
+ @After
+ public void tearDown() {
+ webSockets.close();
+ }
+
+ @Test
+ public void withoutHostOrSessionIdFromServer_usesConfiguredHost() {
+ assertThat(createConnectionUrl(/* cachedHost= */ null, /* lastSessionId= */ null))
+ .isEqualTo("wss://test.firebaseio.com/.ws?ns=test&v=5");
+ }
+
+ @Test
+ public void withValidHostAndSessionIdFromServer_usesBoth() {
+ assertThat(createConnectionUrl(SERVER_HOST, "session-id"))
+ .isEqualTo("wss://s-usc1b-nss-2073.firebaseio.com/.ws?ns=test&v=5&ls=session-id");
+ }
+
+ @Test
+ public void withHostFromServerThatIsNotValidInUrl_usesConfiguredHost() {
+ assertThat(createConnectionUrl("bad host", "session-id"))
+ .isEqualTo("wss://test.firebaseio.com/.ws?ns=test&v=5&ls=session-id");
+ }
+
+ @Test
+ public void withSessionIdFromServerThatIsNotValidInUrl_omitsSessionId() {
+ assertThat(createConnectionUrl(SERVER_HOST, "a b"))
+ .isEqualTo("wss://s-usc1b-nss-2073.firebaseio.com/.ws?ns=test&v=5");
+ }
+
+ @Test
+ public void withHostAndSessionIdFromServerThatAreNotValidInUrl_usesConfiguredHost() {
+ assertThat(createConnectionUrl("a|b.firebaseio.com", "a b"))
+ .isEqualTo("wss://test.firebaseio.com/.ws?ns=test&v=5");
+ }
+
+ /**
+ * Creates a websocket connection the way a reconnect does, and returns the URL that it would
+ * connect to.
+ */
+ private String createConnectionUrl(String cachedHost, String lastSessionId) {
+ new WebsocketConnection(
+ context,
+ HOST_INFO,
+ cachedHost,
+ "test-app-check-token",
+ mock(WebsocketConnection.Delegate.class),
+ lastSessionId);
+ assertThat(urls).hasSize(1);
+ return urls.get(0).toString();
+ }
+}