From ddba21002521e82618c76ceb9eafb1de7735357b Mon Sep 17 00:00:00 2001 From: Yannik Bretschneider Date: Sat, 5 Jun 2021 01:34:53 +0200 Subject: [PATCH] feat: partial implementation of UserManager --- .../server/netconnector/UserManager.java | 87 ++++++++---- .../server/netconnector/UserMetadata.java | 14 -- .../netconnector/MarvelousServerTest.java | 12 +- .../server/netconnector/UserManagerTest.java | 129 ++++++++++++++++++ 4 files changed, 195 insertions(+), 47 deletions(-) delete mode 100644 Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserMetadata.java create mode 100644 Server/src/test/java/uulm/teamname/marvelous/server/netconnector/UserManagerTest.java diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserManager.java b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserManager.java index 11b3eef..b4f63e8 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserManager.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserManager.java @@ -5,9 +5,8 @@ import org.tinylog.Logger; import uulm.teamname.marvelous.gamelibrary.json.JSON; import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage; import uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage; -import uulm.teamname.marvelous.gamelibrary.messages.client.HelloServerMessage; +import uulm.teamname.marvelous.gamelibrary.messages.client.*; import uulm.teamname.marvelous.server.lobbymanager.Participant; -import uulm.teamname.marvelous.server.netconnector.UserMetadata; import org.java_websocket.WebSocket; @@ -15,21 +14,21 @@ import java.util.*; public class UserManager { - /** A set of users that aren't assigned to lobbies or character selection yet, and their resource descriptors */ - private final HashMap newUsers; + /** A set of users that aren't assigned to lobbies or character selection yet */ + private final HashSet newUsers; /** A set of users that can reconnect if they wish to do so, and their matching Participants */ private final HashMap readyToReconnect; /** A set of users that only have to send the * {@link uulm.teamname.marvelous.gamelibrary.messages.client.PlayerReadyMessage} to be assigned - * to a lobby + * to a lobby, containing WebSockets mapped to their user's usernames */ - private final HashMap readyToConnect; + private final HashMap readyToConnect; - /** A set of users that are already assigned to lobbies or character selection */ - private final HashSet inGame; + /** A set of users that are already assigned to lobbies or character selection, mapped to their participants */ + private final HashMap inGame; /** * A map mapping {@link SUID SUIDs} to {@link Participant Participants} to assert whether (and where to) reconnect @@ -41,19 +40,18 @@ public class UserManager { /** Constructs a new, empty UserManager */ public UserManager(JSON json) { - this.newUsers = new HashMap<>(); + this.newUsers = new HashSet<>(); this.readyToConnect = new HashMap<>(); this.readyToReconnect = new HashMap<>(); - this.inGame = new HashSet<>(); + this.inGame = new HashMap<>(); this.activeParticipants = new HashMap<>(); this.json = json; } /** Called on a new WebSocket connection. Places the WebSocket and its ResourceDescriptor in a HashMap. */ public void connectUser(WebSocket conn, ClientHandshake handshake) { - var metadata = new UserMetadata(conn, handshake.getResourceDescriptor()); synchronized (newUsers) { - newUsers.put(conn, metadata); + newUsers.add(conn); } } @@ -73,7 +71,7 @@ public class UserManager { } void handshake(WebSocket conn, HelloServerMessage message) { - if (!newUsers.containsKey(conn)) { + if (!newUsers.contains(conn)) { Logger.debug("websocket {} sent HelloServerMessage outside of handshake", conn); sendError(conn, "Invalid message, as Handshake is already completed"); return; @@ -90,20 +88,46 @@ public class UserManager { readyToReconnect.put(conn, participant); } } else { - var metadata = newUsers.get(conn); - metadata.username = message.name; - Logger.trace("removing handshaking user from newUsers"); synchronized (newUsers) { newUsers.remove(conn); } Logger.trace("adding handshaking user to readyToConnect"); synchronized (readyToConnect) { - readyToConnect.put(conn, metadata); + readyToConnect.put(conn, clientID); } } } + public void reconnectClient(WebSocket conn, ReconnectMessage message) { + if (!readyToConnect.containsKey(conn)) { + Logger.debug("Non-reconnect-allowed client has sent reconnect message, sending error"); + sendError(conn, "Reconnect is not possible"); + return; + } + Logger.info("Reconnecting client {} to their lobby"); + var participantToRestore = readyToReconnect.get(conn); + synchronized (participantToRestore) { + participantToRestore.setConnection(conn); + } + synchronized (readyToReconnect) { + readyToReconnect.remove(conn); + } + synchronized (inGame) { + inGame.put(conn, participantToRestore); + } + // activeParticipants remains the same, as no players have been removed from the game + } + + public void assignLobby(WebSocket conn, PlayerReadyMessage message) { + } + + public void charactersSelected(WebSocket conn, CharacterSelectionMessage message) { + } + + public void relayRequestMessage(Participant conn, RequestMessage message) { + } + /** Sends an {@link uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage} to the specified user. */ public void sendError(WebSocket conn, String error) { Logger.debug("Sending error message '{}' to WebSocket {}", error, conn); @@ -123,33 +147,38 @@ public class UserManager { } public boolean isUserConnected(WebSocket user) { - return newUsers.containsKey(user) || + return newUsers.contains(user) || readyToReconnect.containsKey(user) || readyToConnect.containsKey(user) || - inGame.contains(user); + inGame.containsKey(user); } public int getUserCount() { return newUsers.size() + readyToConnect.size() + readyToReconnect.size() + inGame.size(); } - public Map getNewUsers() { - return Collections.unmodifiableMap(newUsers); + /** Package-private getter for mutable newUsers HashSet, meant for testing */ + HashSet getNewUsers() { + return newUsers; } - public Map getReadyToReconnect() { - return Collections.unmodifiableMap(readyToReconnect); + /** Package-private getter for mutable readyToReconnect HashMap, meant for testing */ + HashMap getReadyToReconnect() { + return readyToReconnect; } - public Map getReadyToConnect() { - return Collections.unmodifiableMap(readyToConnect); + /** Package-private getter for mutable readyToConnect HashMap, meant for testing */ + HashMap getReadyToConnect() { + return readyToConnect; } - public Set getInGame() { - return Collections.unmodifiableSet(inGame); + /** Package-private getter for mutable inGame HashMap, meant for testing */ + HashMap getInGame() { + return inGame; } - public Map getActiveParticipants() { - return Collections.unmodifiableMap(activeParticipants); + /** Package-private getter for mutable activeParticipants HashMap, meant for testing */ + HashMap getActiveParticipants() { + return activeParticipants; } } diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserMetadata.java b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserMetadata.java deleted file mode 100644 index 535a238..0000000 --- a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserMetadata.java +++ /dev/null @@ -1,14 +0,0 @@ -package uulm.teamname.marvelous.server.netconnector; - -import org.java_websocket.WebSocket; - -class UserMetadata { - public String username; - public final WebSocket conn; - public final String resourceDescriptor; - - public UserMetadata(WebSocket conn, String resourceDescriptor) { - this.conn = conn; - this.resourceDescriptor = resourceDescriptor; - } -} diff --git a/Server/src/test/java/uulm/teamname/marvelous/server/netconnector/MarvelousServerTest.java b/Server/src/test/java/uulm/teamname/marvelous/server/netconnector/MarvelousServerTest.java index 24f3752..1c2aa2b 100644 --- a/Server/src/test/java/uulm/teamname/marvelous/server/netconnector/MarvelousServerTest.java +++ b/Server/src/test/java/uulm/teamname/marvelous/server/netconnector/MarvelousServerTest.java @@ -1,8 +1,11 @@ package uulm.teamname.marvelous.server.netconnector; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.net.InetSocketAddress; + import static org.mockito.Mockito.*; class MarvelousServerTest { @@ -15,8 +18,9 @@ class MarvelousServerTest { } @Test - void mockitoTestThing() { - // verify(server).onMessage(null, "hi"); - + void runServerForTesting() throws InterruptedException { + // server = new MarvelousServer(new InetSocketAddress(1234)); + // Thread serverThread = new Thread(() -> server.start()); + // serverThread.join(); } -} \ No newline at end of file +} diff --git a/Server/src/test/java/uulm/teamname/marvelous/server/netconnector/UserManagerTest.java b/Server/src/test/java/uulm/teamname/marvelous/server/netconnector/UserManagerTest.java new file mode 100644 index 0000000..b15c491 --- /dev/null +++ b/Server/src/test/java/uulm/teamname/marvelous/server/netconnector/UserManagerTest.java @@ -0,0 +1,129 @@ +package uulm.teamname.marvelous.server.netconnector; + +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.ClientHandshakeBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import uulm.teamname.marvelous.gamelibrary.json.JSON; +import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType; +import uulm.teamname.marvelous.gamelibrary.messages.client.*; +import uulm.teamname.marvelous.server.lobbymanager.Participant; + +import java.util.HashMap; +import java.util.Iterator; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; + +class UserManagerTest { + + UserManager manager; + WebSocket connection; + Participant participant; + ClientHandshake handshake; + + @BeforeEach + void beforeEach() { + connection = mock(WebSocket.class); + // when(connection.hashCode()).thenReturn(1); // just for testing, of course + manager = spy(new UserManager(new JSON(null))); // FIXME: This shouldn't be null, of course + + handshake = mock(ClientHandshake.class); + when(handshake.getResourceDescriptor()).thenReturn("/someDescriptor"); + + participant = spy(new Participant(connection, ParticipantType.PlayerOne, false)); + } + + @Test + void userIsConnectedTest() { + assertThat(manager.getUserCount()).isEqualTo(0); + assertThat(manager.getNewUsers()).doesNotContain(connection); + manager.connectUser(connection, handshake); + assertThat(manager.getUserCount()).isEqualTo(1); + assertThat(manager.isUserConnected(connection)).isTrue(); + assertThat(manager.getNewUsers()).containsOnly(connection); + } + + @Test + void helloServerMessagesGetAssignedProperly() { + manager.messageReceived( + connection, + "{\"messageType\":\"HELLO_SERVER\",\"name\":\"SomeAwesomeName\",\"deviceID\":\"YAY\"}"); + + verify(manager).handshake( + eq(connection), + any(HelloServerMessage.class)); + } + + @Test + void handshakeGetsCompletedProperlyTest() { + manager.connectUser(connection, handshake); + assertThat(manager.getUserCount()).isEqualTo(1); + assertThat(manager.getNewUsers()).containsOnly(connection); + assertThat(manager.getReadyToConnect()).isEmpty(); + + var message = new HelloServerMessage(); + message.name = "Some Awesome Name"; + message.deviceID = "Some Interesting ID"; + + manager.handshake(connection, message); + + assertThat(manager.getUserCount()).isEqualTo(1); + assertThat(manager.getNewUsers()).isEmpty(); + assertThat(manager.getReadyToConnect()).containsOnlyKeys(connection); + + } + + void reconnectMessagesGetAssignedProperly() { + manager.messageReceived( + connection, + "{\"messageType\":\"RECONNECT\",\"reconnect\":\"TRUE\"}"); + + verify(manager).reconnectClient( + eq(connection), + any(ReconnectMessage.class)); + } + + @Test + @Disabled + void reconnectWorksProperly() { + // TODO: create test for this case + } + + void playerReadyMessagesGetAssignedProperly() { + manager.messageReceived( + connection, + "{\"messageType\":\"PLAYER_READY\",\"startGame\":\"false\",\"role\":\"Player\"}"); + + verify(manager).assignLobby( + eq(connection), + any(PlayerReadyMessage.class)); + } + + + void characterSelectionMessageGetsAssignedProperly() { + manager.messageReceived( + connection, + "{\"messageType\":\"CHARACTER_SELECTION\",\"characters\":" + + "[true, true, false, true, false, false, true, true, false, false, false, true]}"); + + verify(manager).charactersSelected( + eq(connection), + any(CharacterSelectionMessage.class)); + } + + void requestMessagesGetRelayedProperly() { + manager.messageReceived( + connection, + "{\"messageType\":\"REQUESTS\",\"messages\":[]}"); + + verify(manager).relayRequestMessage( + eq(participant), + any(RequestMessage.class)); + } +}