using System; using System.Collections; using System.Collections.Generic; using NUnit.Framework; using Unity.BossRoom.ConnectionManagement; using Unity.BossRoom.Infrastructure; using Unity.BossRoom.UnityServices; using Unity.BossRoom.UnityServices.Lobbies; using Unity.BossRoom.Utils; using Unity.Multiplayer.Samples.Utilities; using Unity.Netcode; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; using VContainer; using VContainer.Unity; namespace Unity.BossRoom.Tests.Runtime { public class ConnectionManagementTests : NetcodeIntegrationTest { const string k_FailedToConnectToServerErrorMessage = "Failed to connect to server."; class ConnectionManagementTestsLifeTimeScope : LifetimeScope { public NetworkManager NetworkManager; public ConnectionManager ConnectionManager; public UpdateRunner UpdateRunner; protected override void Awake() { } protected override void Configure(IContainerBuilder builder) { builder.RegisterComponent(NetworkManager); builder.RegisterComponent(ConnectionManager); builder.RegisterComponent(UpdateRunner); builder.RegisterComponent(new NetworkedMessageChannel()).AsImplementedInterfaces(); builder.RegisterInstance(new BufferedMessageChannel()).AsImplementedInterfaces(); builder.RegisterInstance(new MessageChannel()).AsImplementedInterfaces(); builder.RegisterInstance(new MessageChannel()).AsImplementedInterfaces(); builder.RegisterInstance(new BufferedMessageChannel()).AsImplementedInterfaces(); builder.Register(Lifetime.Singleton); builder.Register(Lifetime.Singleton); builder.Register(Lifetime.Singleton); builder.RegisterEntryPoint(Lifetime.Singleton).AsSelf(); } } class SceneLoaderWrapperStub : SceneLoaderWrapper { public override void Awake() { Instance = this; } public override void Start() { } public override void LoadScene(string sceneName, bool useNetworkSceneManager, LoadSceneMode loadSceneMode = LoadSceneMode.Single) { } } protected override int NumberOfClients => 2; ConnectionManagementTestsLifeTimeScope[] m_ClientScopes; ConnectionManagementTestsLifeTimeScope m_ServerScope; ConnectionManager[] m_ClientConnectionManagers; ConnectionManager m_ServerConnectionManager; protected override bool CanStartServerAndClients() { return false; } static void InitializeInstance(string name, NetworkManager networkManager, out ConnectionManager connectionManager, out ConnectionManagementTestsLifeTimeScope scope) { var connectionManagerGO = new GameObject($"ConnectionManager - {name}"); connectionManager = connectionManagerGO.AddComponent(); networkManager.NetworkConfig.ConnectionApproval = true; networkManager.NetworkConfig.EnableSceneManagement = false; var updateRunnerGO = new GameObject($"UpdateRunner - {name}"); var updateRunner = updateRunnerGO.AddComponent(); var lifeTimeScopeGO = new GameObject($"LifeTimeScope - {name}"); scope = lifeTimeScopeGO.AddComponent(); scope.NetworkManager = networkManager; scope.ConnectionManager = connectionManager; scope.UpdateRunner = updateRunner; scope.Build(); } void CreatePlayerPrefab() { // Create playerPrefab m_PlayerPrefab = new GameObject("Player"); NetworkObject networkObject = m_PlayerPrefab.AddComponent(); // Make it a prefab NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); // Set the player prefab for the server and clients m_ServerNetworkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab; foreach (var client in m_ClientNetworkManagers) { client.NetworkConfig.PlayerPrefab = m_PlayerPrefab; } } protected override void OnServerAndClientsCreated() { var sceneLoaderWrapperGO = new GameObject("SceneLoader"); sceneLoaderWrapperGO.AddComponent(); m_ClientScopes = new ConnectionManagementTestsLifeTimeScope[NumberOfClients]; m_ClientConnectionManagers = new ConnectionManager[NumberOfClients]; for (var i = 0; i < NumberOfClients; i++) { var clientId = i; InitializeInstance($"Client{clientId}", m_ClientNetworkManagers[clientId], out m_ClientConnectionManagers[clientId], out m_ClientScopes[clientId]); } InitializeInstance("Server", m_ServerNetworkManager, out m_ServerConnectionManager, out m_ServerScope); CreatePlayerPrefab(); base.OnServerAndClientsCreated(); } protected override IEnumerator OnTearDown() { m_ServerConnectionManager.RequestShutdown(); yield return new WaitWhile(() => m_ServerNetworkManager.IsListening); for (var i = 0; i < NumberOfClients; i++) { var clientId = i; m_ClientConnectionManagers[clientId].RequestShutdown(); yield return new WaitWhile(() => m_ClientNetworkManagers[clientId].IsListening); } m_ServerScope.Dispose(); for (var i = 0; i < NumberOfClients; i++) { m_ClientScopes[i].Dispose(); } foreach (var sceneGameObject in GameObject.FindObjectsOfType()) { GameObject.DestroyImmediate(sceneGameObject); } yield return base.OnTearDown(); } void StartHost() { m_ServerConnectionManager.StartHostIp("server", "127.0.0.1", 9998); } IEnumerator ConnectClients() { for (var i = 0; i < NumberOfClients; i++) { m_ClientConnectionManagers[i].StartClientIp($"client{i}", "127.0.0.1", 9998); } yield return WaitForClientsConnectedOrTimeOut(m_ClientNetworkManagers); } void SetUniqueProfilesForEachClient() { for (var i = 0; i < NumberOfClients; i++) { var profileManager = m_ClientScopes[i].Container.Resolve(); profileManager.Profile = $"Client{i}"; } } void AssertHostIsListening() { Assert.IsTrue(m_ServerNetworkManager.IsHost && m_ServerNetworkManager.IsListening, "Host not listening."); } void AssertAllClientsAreConnected() { for (var i = 0; i < NumberOfClients; i++) { Assert.IsTrue(m_ClientNetworkManagers[i].IsConnectedClient, $"Client{i} is not connected."); } } [Test] public void StartHost_Success() { StartHost(); AssertHostIsListening(); } [UnityTest] public IEnumerator StartHostAndConnectClients_Success() { StartHost(); AssertHostIsListening(); SetUniqueProfilesForEachClient(); yield return ConnectClients(); AssertAllClientsAreConnected(); } [UnityTest] public IEnumerator UserRequestedHostShutdownAfterClientsConnected_ClientsDisconnectedWithReason() { StartHost(); AssertHostIsListening(); SetUniqueProfilesForEachClient(); yield return ConnectClients(); AssertAllClientsAreConnected(); var nbHostEndedSessionMsgsReceived = 0; var subscriptions = new DisposableGroup(); for (int i = 0; i < NumberOfClients; i++) { m_ClientScopes[i].Container.Resolve>().Subscribe(message => { // ignoring the first success message that is in the buffer if (message != ConnectStatus.Success) { Assert.AreEqual(ConnectStatus.HostEndedSession, message, "Received unexpected ConnectStatus message."); nbHostEndedSessionMsgsReceived++; } }); } m_ServerConnectionManager.RequestShutdown(); yield return new WaitWhile(() => m_ServerNetworkManager.IsListening); Assert.IsFalse(m_ServerNetworkManager.IsHost && m_ServerNetworkManager.IsListening, "Host has not properly shut down."); for (var i = 0; i < NumberOfClients; i++) { var clientId = i; yield return new WaitWhile(() => m_ClientNetworkManagers[clientId].IsListening); Assert.IsFalse(m_ClientNetworkManagers[clientId].IsConnectedClient); } Assert.AreEqual(NumberOfClients, nbHostEndedSessionMsgsReceived, "Not all clients received a HostEndedSession message."); subscriptions.Dispose(); } [UnityTest] public IEnumerator AttemptingToConnectWithSamePlayerId_ClientsDisconnectedWithReason() { StartHost(); AssertHostIsListening(); var nbLoggedInAgainMsgsReceived = 0; var subscriptions = new DisposableGroup(); // setting the same profile for all clients so that they have the same player ID for (var i = 0; i < NumberOfClients; i++) { var profileManager = m_ClientScopes[i].Container.Resolve(); profileManager.Profile = $"Client"; if (i > 0) { subscriptions.Add(m_ClientScopes[i].Container.Resolve>().Subscribe(message => { Assert.AreEqual(ConnectStatus.LoggedInAgain, message, "Received unexpected ConnectStatus message."); nbLoggedInAgainMsgsReceived++; })); } } yield return ConnectClients(); // The first client should be able to connect Assert.IsTrue(m_ClientNetworkManagers[0].IsConnectedClient, "The first client is not connected."); // Every other client should get their connection denied for (var i = 1; i < NumberOfClients; i++) { Assert.IsFalse(m_ClientNetworkManagers[i].IsConnectedClient, "A client with the same player ID has connected."); } Assert.AreEqual(NumberOfClients - 1, nbLoggedInAgainMsgsReceived, "Not all clients received a LoggedInAgain message."); subscriptions.Dispose(); } [UnityTest] public IEnumerator ClientConnectingWithoutHost_ConnectionFailed() { SetUniqueProfilesForEachClient(); for (var i = 0; i < NumberOfClients; i++) { LogAssert.Expect(LogType.Error, k_FailedToConnectToServerErrorMessage); } yield return ConnectClients(); for (var i = 0; i < NumberOfClients; i++) { Assert.IsFalse(m_ClientNetworkManagers[i].IsConnectedClient, $"Client{i} is connected while no server is running."); } } [UnityTest] public IEnumerator UnexpectedClientDisconnect_ClientReconnectingSuccessfully() { StartHost(); AssertHostIsListening(); SetUniqueProfilesForEachClient(); yield return ConnectClients(); AssertAllClientsAreConnected(); // wait for client synchronization to be over server-side before shutting down client yield return null; yield return null; var nbReconnectingMsgsReceived = 0; var subscriptions = new DisposableGroup(); subscriptions.Add(m_ClientScopes[0].Container.Resolve>().Subscribe(message => { // ignoring the first success message that is in the buffer if (message != ConnectStatus.Success) { Assert.AreEqual(ConnectStatus.Reconnecting, message, "Received unexpected ConnectStatus message."); nbReconnectingMsgsReceived++; } })); // Disconnecting the client at the transport level m_ClientNetworkManagers[0].NetworkConfig.NetworkTransport.DisconnectLocalClient(); // Waiting until shutdown is complete yield return new WaitWhile(() => m_ClientNetworkManagers[0].ShutdownInProgress); Assert.IsFalse(m_ClientNetworkManagers[0].IsConnectedClient, "Client0 has not shut down properly."); // Waiting for client to automatically reconnect yield return WaitForClientsConnectedOrTimeOut(); Assert.IsTrue(m_ClientNetworkManagers[0].IsConnectedClient, "Client0 failed to reconnect."); Assert.AreEqual(1, nbReconnectingMsgsReceived, "No Reconnecting message received."); subscriptions.Dispose(); } #if !NETCODEFORGAMEOBJECTS_1_5_2_OR_1_6_0 [UnityTest] public IEnumerator UnexpectedServerShutdown_ClientsFailToReconnect() { StartHost(); AssertHostIsListening(); SetUniqueProfilesForEachClient(); yield return ConnectClients(); AssertAllClientsAreConnected(); var nbReconnectingMsgsReceived = 0; var nbGenericDisconnectMsgReceived = 0; var subscriptions = new DisposableGroup(); for (int i = 0; i < NumberOfClients; i++) { subscriptions.Add(m_ClientScopes[i].Container.Resolve>().Subscribe(message => { // ignoring the first success message that is in the buffer if (message != ConnectStatus.Success) { var possibleMessages = new List(); possibleMessages.Add(ConnectStatus.Reconnecting); possibleMessages.Add(ConnectStatus.GenericDisconnect); Assert.Contains(message, possibleMessages, "Received unexpected ConnectStatus message."); if (message == ConnectStatus.Reconnecting) { nbReconnectingMsgsReceived++; } else if (message == ConnectStatus.GenericDisconnect) { nbGenericDisconnectMsgReceived++; } } })); } // Shutting down the server m_ServerNetworkManager.Shutdown(); yield return new WaitWhile(() => m_ServerNetworkManager.ShutdownInProgress); Assert.IsFalse(m_ServerNetworkManager.IsListening, "Server has not shut down properly."); // Waiting until shutdown is complete for the clients as well for (var i = 0; i < NumberOfClients; i++) { var clientId = i; yield return new WaitWhile(() => m_ClientNetworkManagers[clientId].ShutdownInProgress); Assert.IsFalse(m_ClientNetworkManagers[clientId].IsConnectedClient, $"Client{clientId} has not shut down properly after losing connection."); } var maxNbReconnectionAttempts = 0; for (var i = 0; i < NumberOfClients; i++) { var nbReconnectionAttempts = m_ClientConnectionManagers[i].NbReconnectAttempts; maxNbReconnectionAttempts = Math.Max(maxNbReconnectionAttempts, nbReconnectionAttempts); for (var j = 0; j < nbReconnectionAttempts; j++) { // Expecting this error for each reconnection attempt for each client LogAssert.Expect(LogType.Error, k_FailedToConnectToServerErrorMessage); } } // Waiting for clients to fail to automatically reconnect. We wait once for each reconnection attempt. for (var i = 0; i < maxNbReconnectionAttempts; i++) { yield return WaitForClientsConnectedOrTimeOut(); for (var j = 0; j < NumberOfClients; j++) { Assert.IsFalse(m_ClientNetworkManagers[j].IsConnectedClient, $"Client{j} is connected while no server is running."); } } Assert.AreEqual(NumberOfClients, nbReconnectingMsgsReceived, "Not all clients received a Reconnecting message."); Assert.AreEqual(NumberOfClients, nbGenericDisconnectMsgReceived, "Not all clients received a GenericDisconnect message."); subscriptions.Dispose(); } #endif [UnityTest] public IEnumerator ClientAndHostChangingRolesBetweenSessions_Success() { SetUniqueProfilesForEachClient(); StartHost(); AssertHostIsListening(); yield return ConnectClients(); AssertAllClientsAreConnected(); // Requesting shutdown on the server m_ServerConnectionManager.RequestShutdown(); yield return new WaitWhile(() => m_ServerNetworkManager.IsListening); Assert.IsFalse(m_ServerNetworkManager.IsHost); // Waiting until shutdown is complete for the clients for (var i = 0; i < NumberOfClients; i++) { var clientId = i; yield return new WaitWhile(() => m_ClientNetworkManagers[clientId].ShutdownInProgress); Assert.IsFalse(m_ClientNetworkManagers[clientId].IsConnectedClient, $"Client{clientId} has not shut down properly after losing connection."); } // Switching references for Client0 and Server temporarily (m_ServerNetworkManager, m_ClientNetworkManagers[0]) = (m_ClientNetworkManagers[0], m_ServerNetworkManager); (m_ServerConnectionManager, m_ClientConnectionManagers[0]) = (m_ClientConnectionManagers[0], m_ServerConnectionManager); // recreate player prefab here since the GameObject has been destroyed CreatePlayerPrefab(); StartHost(); AssertHostIsListening(); yield return ConnectClients(); AssertAllClientsAreConnected(); } [UnityTest] public IEnumerator ClientCancellingWhileConnectingToListeningServer_ConnectionCancelled() { StartHost(); AssertHostIsListening(); SetUniqueProfilesForEachClient(); for (var i = 0; i < NumberOfClients; i++) { m_ClientConnectionManagers[i].StartClientIp($"client{i}", "127.0.0.1", 9998); } m_ClientConnectionManagers[0].RequestShutdown(); yield return WaitForClientsConnectedOrTimeOut(); Assert.IsFalse(m_ClientNetworkManagers[0].IsConnectedClient, "Client0 has not successfully cancelled its connection."); for (var i = 1; i < NumberOfClients; i++) { Assert.IsTrue(m_ClientNetworkManagers[i].IsConnectedClient, $"Client{i} is not connected."); } } [UnityTest] public IEnumerator ClientCancellingWhileConnectingToNonExistingServer_NoConnectionError() { SetUniqueProfilesForEachClient(); for (var i = 0; i < NumberOfClients; i++) { m_ClientConnectionManagers[i].StartClientIp($"client{i}", "127.0.0.1", 9998); } m_ClientConnectionManagers[0].RequestShutdown(); for (var i = 1; i < NumberOfClients; i++) { // expecting that error for every client except the one that cancelled its connection LogAssert.Expect(LogType.Error, k_FailedToConnectToServerErrorMessage); } yield return WaitForClientsConnectedOrTimeOut(); for (var i = 0; i < NumberOfClients; i++) { Assert.IsFalse(m_ClientNetworkManagers[i].IsConnectedClient, $"Client{i} is connected while no server is running."); } } } }