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 b2a774c..9bfaa1b 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 @@ -45,7 +45,7 @@ public class UserManager { } /** Called on a new WebSocket connection. Places the WebSocket and its ResourceDescriptor in a HashMap. */ - void connectUser(WebSocket conn) { + public void connectUser(WebSocket conn) { Logger.debug("Connected new user"); synchronized(clients) { clients.put(conn, new Client(conn)); @@ -57,7 +57,7 @@ public class UserManager { * @param conn is the {@link WebSocket} that sent the message * @param message is the {@link String} sent by the connection */ - void messageReceived(WebSocket conn, String message) { + public void messageReceived(WebSocket conn, String message) { Logger.debug("Message received from {}", conn); Client client = clients.get(conn); @@ -90,7 +90,7 @@ public class UserManager { } /** Called on closed connection. */ - void disconnectUser(WebSocket conn, boolean closedByRemote) { + public void disconnectUser(WebSocket conn, boolean closedByRemote) { Logger.info("Disconnecting client '{}'", conn); Client client = clients.get(conn); diff --git a/Server/src/test/java/uulm/teamname/marvelous/server/BaseGameLogicTest.java b/Server/src/test/java/uulm/teamname/marvelous/server/BaseGameLogicTest.java new file mode 100644 index 0000000..2ea0db9 --- /dev/null +++ b/Server/src/test/java/uulm/teamname/marvelous/server/BaseGameLogicTest.java @@ -0,0 +1,81 @@ +package uulm.teamname.marvelous.server; + +import uulm.teamname.marvelous.gamelibrary.config.*; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.ThreadLocalRandom; + +public class BaseGameLogicTest { + protected static final Iterator randomIntegers = ThreadLocalRandom.current().ints().iterator(); + + protected static final PartyConfig partyConfig = new PartyConfig(); + protected static final CharacterConfig characterConfig = new CharacterConfig(); + protected static final ScenarioConfig scenarioConfig = new ScenarioConfig(); + + protected static final ArrayList player1Selection = new ArrayList<>(); + protected static final ArrayList player2Selection = new ArrayList<>(); + + protected static void generate() { + partyConfig.maxRounds = 50; + partyConfig.mindStoneCD = 2; + partyConfig.powerStoneCD = 3; + partyConfig.realityStoneCD = 4; + partyConfig.soulStoneCD = 5; + partyConfig.spaceStoneCD = 6; + partyConfig.timeStoneCD = 7; + partyConfig.mindStoneDMG = 3; + + characterConfig.characters = new CharacterProperties[24]; + for(int i = 0; i < characterConfig.characters.length; i++) { + characterConfig.characters[i] = generateCharacter(i); + } + + scenarioConfig.name = generateName(20); + scenarioConfig.author = generateName(20); + scenarioConfig.scenario = new FieldType[20][20]; + for(int x = 0; x < scenarioConfig.scenario[0].length; x++) { + for(int y = 0; y < scenarioConfig.scenario.length; y++) { + if(Math.abs(randomIntegers.next() % 100) < 10) { + scenarioConfig.scenario[y][x] = FieldType.ROCK; + }else { + scenarioConfig.scenario[y][x] = FieldType.GRASS; + } + } + } + + for(int i = 0; i < 6; i++) { + player1Selection.add(i); + } + + for(int i = 6; i < 12; i++) { + player2Selection.add(i); + } + } + + private static String generateName(int length) { + StringBuilder name = new StringBuilder(); + for (int j = 0; j < length; j++) { + name.append((char) ( + 65 + Math.abs(randomIntegers.next() % 26) + 32 * Math.abs(randomIntegers.next() % 2) + )); + } + return name.toString(); + } + + private static CharacterProperties generateCharacter(int id) { + CharacterProperties props = new CharacterProperties(); + + props.characterID = id; + props.name = generateName(10); + + props.HP = Math.abs(randomIntegers.next() % 15) + 5; + props.MP = Math.abs(randomIntegers.next() % 5) + 2; + props.AP = Math.abs(randomIntegers.next() % 5) + 2; + props.meleeDamage = Math.abs(randomIntegers.next() % 5) + 2; + props.rangedDamage = Math.abs(randomIntegers.next() % 5) + 2; + props.attackRange = Math.abs(randomIntegers.next() % 5) + 2; + + return props; + } +} diff --git a/Server/src/test/java/uulm/teamname/marvelous/server/MarvelousServerTest.java b/Server/src/test/java/uulm/teamname/marvelous/server/MarvelousServerTest.java new file mode 100644 index 0000000..f7fbfa7 --- /dev/null +++ b/Server/src/test/java/uulm/teamname/marvelous/server/MarvelousServerTest.java @@ -0,0 +1,198 @@ +package uulm.teamname.marvelous.server; + +import org.java_websocket.WebSocket; +import org.junit.jupiter.api.*; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.tinylog.configuration.Configuration; +import uulm.teamname.marvelous.gamelibrary.json.JSON; +import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage; +import uulm.teamname.marvelous.gamelibrary.messages.RoleEnum; +import uulm.teamname.marvelous.gamelibrary.messages.client.CharacterSelectionMessage; +import uulm.teamname.marvelous.gamelibrary.messages.client.HelloServerMessage; +import uulm.teamname.marvelous.gamelibrary.messages.client.PlayerReadyMessage; +import uulm.teamname.marvelous.gamelibrary.messages.server.*; +import uulm.teamname.marvelous.server.lobbymanager.LobbyManager; +import uulm.teamname.marvelous.server.netconnector.UserManager; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.mockito.Mockito.*; + +class MarvelousServerTest extends BaseGameLogicTest { + private static MockedStatic serverMock; + private static JSON json; + + @BeforeAll + static void start() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + Constructor um = UserManager.class.getDeclaredConstructor(); + um.setAccessible(true); + um.newInstance(); + Constructor lm = LobbyManager.class.getDeclaredConstructor(); + lm.setAccessible(true); + lm.newInstance(); + + serverMock = Mockito.mockStatic(Server.class); + + generate(); + + serverMock.when(Server::getMaxLobbies).thenReturn(10); + serverMock.when(Server::getPartyConfig).thenReturn(partyConfig); + serverMock.when(Server::getScenarioConfig).thenReturn(scenarioConfig); + serverMock.when(Server::getCharacterConfig).thenReturn(characterConfig); + + json = new JSON(characterConfig); + + Map map = new HashMap<>(); + Configuration.replace(map); + map.put("writer1", "console"); + map.put("writer1.level", "trace"); + map.put("writer1.format", "[{thread}] {level}: {message}"); + Configuration.replace(map); + } + + + @Test + void main() { + UserManager m = UserManager.getInstance(); + + WebSocket p1 = mock(WebSocket.class); + WebSocket p2 = mock(WebSocket.class); + WebSocket s1 = mock(WebSocket.class); + WebSocket s2 = mock(WebSocket.class); + + m.connectUser(p1); + ensureHandshake(m, p1, "Player 1", "1234", false); + + m.connectUser(p2); + ensureHandshake(m, p2, "Player 2", "4321", false); + + m.connectUser(s1); + ensureHandshake(m, s1, "Spectator 1", "3333", false); + + m.connectUser(s2); + ensureHandshake(m, s2, "Spectator 2", "4444", false); + + ensurePlayerReady(m, p1, true, RoleEnum.PLAYER); + ensurePlayerReady(m, p2, true, RoleEnum.PLAYER); + ensureSpectatorReady(m, s1, true, RoleEnum.SPECTATOR); + + //these are broken right now because Server doesn't get mocked in lobby threads + /* + ensureCharacterSelection(m, p1, false); + ensureCharacterSelection(m, p2, true); + + ensureReceived(p1, new GameStructureMessage()); + ensureReceived(p2, new GameStructureMessage()); + ensureReceived(s1, new GameStructureMessage()); + */ + } + + private void ensureHandshake(UserManager m, WebSocket c, String name, String deviceID, boolean runningGame) { + HelloServerMessage message = new HelloServerMessage(); + message.name = name; + message.deviceID = deviceID; + + HelloClientMessage response = new HelloClientMessage(); + response.runningGame = runningGame; + + ensureResponse(m, c, message, response); + } + + private void ensurePlayerReady(UserManager m, WebSocket c, boolean startGame, RoleEnum role) { + PlayerReadyMessage message = new PlayerReadyMessage(); + message.startGame = startGame; + message.role = role; + + GameAssignmentMessage response = new GameAssignmentMessage(); + //properties are left null because we can't test their content (they are random) + + ensureResponse(m, c, message, response); + } + + private void ensureSpectatorReady(UserManager m, WebSocket c, boolean startGame, RoleEnum role) { + PlayerReadyMessage message = new PlayerReadyMessage(); + message.startGame = startGame; + message.role = role; + + GeneralAssignmentMessage response = new GeneralAssignmentMessage(); + //properties are left null because we can't test their content (they are random) + + ensureResponse(m, c, message, response); + } + + private void ensureCharacterSelection(UserManager m, WebSocket c, boolean selectionComplete) { + CharacterSelectionMessage message = new CharacterSelectionMessage(); + message.characters = new Boolean[12]; + for(int i = 0; i < 6; i++) { + message.characters[i] = true; + } + for(int i = 6; i < 12; i++) { + message.characters[i] = false; + } + + ConfirmSelectionMessage response = new ConfirmSelectionMessage(); + response.selectionComplete = selectionComplete; + + ensureResponse(m, c, message, response); + } + + + /** Ensures that the given socket received the given response in response to the given message. */ + private void ensureResponse(UserManager m, WebSocket c, BasicMessage message, BasicMessage response) { + Optional in = json.stringify(message); + + if(in.isPresent()) { + m.messageReceived(c, in.get()); + verify(c).send((String)argThat( + (out)->verifyResponse((String)out, response) + )); + clearInvocations(c); + }else { + throw new IllegalArgumentException("[TEST] Message in test call could not be serialized!"); + } + } + + /** Ensures that the given socket received the given response. */ + private void ensureReceived(WebSocket c, BasicMessage response) { + verify(c).send((String)argThat( + (out)->verifyResponse((String)out, response) + )); + } + + + /** + * Verifies a response message and requires all non-null fields of the expected message + * to equal the respective values in the actual received message. + */ + private boolean verifyResponse(String actual, T expected) { + Optional in = json.parse(actual); + + if(in.isPresent()) { + T message = (T) in.get(); + + for(Field field: expected.getClass().getDeclaredFields()) { + try { + Object value = field.get(expected); + if(value != null && !value.equals(field.get(message))) { + throw new IllegalArgumentException("[TEST] Field " + field.getName() + " expected to be '" + value + "' but was '" + field.get(message) + "'"); + } + }catch(IllegalAccessException ignore) { } + } + + return true; + }else { + throw new IllegalArgumentException("[TEST] Response in test call could not be deserialized!"); + } + } + + @AfterAll + static void stop() { + serverMock.close(); + } +} 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 deleted file mode 100644 index 177f058..0000000 --- a/Server/src/test/java/uulm/teamname/marvelous/server/netconnector/MarvelousServerTest.java +++ /dev/null @@ -1,27 +0,0 @@ -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 { - - MarvelousServer server; - - @BeforeEach - void setUp() { - server = spy(MarvelousServer.class); - } - - @Test - @Disabled - void runServerForTesting() throws InterruptedException { - // server = new MarvelousServer(new InetSocketAddress(1234)); - // Thread serverThread = new Thread(() -> server.start()); - // serverThread.join(); - } -}