feat: partial implementation of UserManager

This commit is contained in:
Yannik Bretschneider 2021-06-05 01:34:53 +02:00
parent cb2ad7e445
commit ddba210025
4 changed files with 195 additions and 47 deletions

View File

@ -5,9 +5,8 @@ import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.json.JSON; import uulm.teamname.marvelous.gamelibrary.json.JSON;
import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage; import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage;
import uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage; 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.lobbymanager.Participant;
import uulm.teamname.marvelous.server.netconnector.UserMetadata;
import org.java_websocket.WebSocket; import org.java_websocket.WebSocket;
@ -15,21 +14,21 @@ import java.util.*;
public class UserManager { public class UserManager {
/** A set of users that aren't assigned to lobbies or character selection yet, and their resource descriptors */ /** A set of users that aren't assigned to lobbies or character selection yet */
private final HashMap<WebSocket, UserMetadata> newUsers; private final HashSet<WebSocket> newUsers;
/** A set of users that can reconnect if they wish to do so, and their matching Participants */ /** A set of users that can reconnect if they wish to do so, and their matching Participants */
private final HashMap<WebSocket, Participant> readyToReconnect; private final HashMap<WebSocket, Participant> readyToReconnect;
/** A set of users that only have to send the /** A set of users that only have to send the
* {@link uulm.teamname.marvelous.gamelibrary.messages.client.PlayerReadyMessage} to be assigned * {@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<WebSocket, UserMetadata> readyToConnect; private final HashMap<WebSocket, SUID> readyToConnect;
/** A set of users that are already assigned to lobbies or character selection */ /** A set of users that are already assigned to lobbies or character selection, mapped to their participants */
private final HashSet<WebSocket> inGame; private final HashMap<WebSocket, Participant> inGame;
/** /**
* A map mapping {@link SUID SUIDs} to {@link Participant Participants} to assert whether (and where to) reconnect * 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 */ /** Constructs a new, empty UserManager */
public UserManager(JSON json) { public UserManager(JSON json) {
this.newUsers = new HashMap<>(); this.newUsers = new HashSet<>();
this.readyToConnect = new HashMap<>(); this.readyToConnect = new HashMap<>();
this.readyToReconnect = new HashMap<>(); this.readyToReconnect = new HashMap<>();
this.inGame = new HashSet<>(); this.inGame = new HashMap<>();
this.activeParticipants = new HashMap<>(); this.activeParticipants = new HashMap<>();
this.json = json; this.json = json;
} }
/** Called on a new WebSocket connection. Places the WebSocket and its ResourceDescriptor in a HashMap. */ /** Called on a new WebSocket connection. Places the WebSocket and its ResourceDescriptor in a HashMap. */
public void connectUser(WebSocket conn, ClientHandshake handshake) { public void connectUser(WebSocket conn, ClientHandshake handshake) {
var metadata = new UserMetadata(conn, handshake.getResourceDescriptor());
synchronized (newUsers) { synchronized (newUsers) {
newUsers.put(conn, metadata); newUsers.add(conn);
} }
} }
@ -73,7 +71,7 @@ public class UserManager {
} }
void handshake(WebSocket conn, HelloServerMessage message) { void handshake(WebSocket conn, HelloServerMessage message) {
if (!newUsers.containsKey(conn)) { if (!newUsers.contains(conn)) {
Logger.debug("websocket {} sent HelloServerMessage outside of handshake", conn); Logger.debug("websocket {} sent HelloServerMessage outside of handshake", conn);
sendError(conn, "Invalid message, as Handshake is already completed"); sendError(conn, "Invalid message, as Handshake is already completed");
return; return;
@ -90,20 +88,46 @@ public class UserManager {
readyToReconnect.put(conn, participant); readyToReconnect.put(conn, participant);
} }
} else { } else {
var metadata = newUsers.get(conn);
metadata.username = message.name;
Logger.trace("removing handshaking user from newUsers"); Logger.trace("removing handshaking user from newUsers");
synchronized (newUsers) { synchronized (newUsers) {
newUsers.remove(conn); newUsers.remove(conn);
} }
Logger.trace("adding handshaking user to readyToConnect"); Logger.trace("adding handshaking user to readyToConnect");
synchronized (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. */ /** Sends an {@link uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage} to the specified user. */
public void sendError(WebSocket conn, String error) { public void sendError(WebSocket conn, String error) {
Logger.debug("Sending error message '{}' to WebSocket {}", error, conn); Logger.debug("Sending error message '{}' to WebSocket {}", error, conn);
@ -123,33 +147,38 @@ public class UserManager {
} }
public boolean isUserConnected(WebSocket user) { public boolean isUserConnected(WebSocket user) {
return newUsers.containsKey(user) || return newUsers.contains(user) ||
readyToReconnect.containsKey(user) || readyToReconnect.containsKey(user) ||
readyToConnect.containsKey(user) || readyToConnect.containsKey(user) ||
inGame.contains(user); inGame.containsKey(user);
} }
public int getUserCount() { public int getUserCount() {
return newUsers.size() + readyToConnect.size() + readyToReconnect.size() + inGame.size(); return newUsers.size() + readyToConnect.size() + readyToReconnect.size() + inGame.size();
} }
public Map<WebSocket, UserMetadata> getNewUsers() { /** Package-private getter for mutable newUsers HashSet, meant for testing */
return Collections.unmodifiableMap(newUsers); HashSet<WebSocket> getNewUsers() {
return newUsers;
} }
public Map<WebSocket, Participant> getReadyToReconnect() { /** Package-private getter for mutable readyToReconnect HashMap, meant for testing */
return Collections.unmodifiableMap(readyToReconnect); HashMap<WebSocket, Participant> getReadyToReconnect() {
return readyToReconnect;
} }
public Map<WebSocket, UserMetadata> getReadyToConnect() { /** Package-private getter for mutable readyToConnect HashMap, meant for testing */
return Collections.unmodifiableMap(readyToConnect); HashMap<WebSocket, SUID> getReadyToConnect() {
return readyToConnect;
} }
public Set<WebSocket> getInGame() { /** Package-private getter for mutable inGame HashMap, meant for testing */
return Collections.unmodifiableSet(inGame); HashMap<WebSocket, Participant> getInGame() {
return inGame;
} }
public Map<SUID, Participant> getActiveParticipants() { /** Package-private getter for mutable activeParticipants HashMap, meant for testing */
return Collections.unmodifiableMap(activeParticipants); HashMap<SUID, Participant> getActiveParticipants() {
return activeParticipants;
} }
} }

View File

@ -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;
}
}

View File

@ -1,8 +1,11 @@
package uulm.teamname.marvelous.server.netconnector; package uulm.teamname.marvelous.server.netconnector;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.net.InetSocketAddress;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
class MarvelousServerTest { class MarvelousServerTest {
@ -15,8 +18,9 @@ class MarvelousServerTest {
} }
@Test @Test
void mockitoTestThing() { void runServerForTesting() throws InterruptedException {
// verify(server).onMessage(null, "hi"); // server = new MarvelousServer(new InetSocketAddress(1234));
// Thread serverThread = new Thread(() -> server.start());
// serverThread.join();
} }
} }

View File

@ -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));
}
}