diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java
index f93ef7c..a9a46c7 100644
--- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java
@@ -63,6 +63,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
+import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyCleanupJob;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
@@ -73,11 +74,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandR
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
-import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
-import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
-import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
-import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import java.time.Duration;
@@ -141,37 +138,12 @@ public class ServerApp {
userRegistry,
new CommandContext(lobbyManager, sessionManager));
- // Periodic cleanup: remove empty lobbies older than 30s and notify affected
- // users
scheduler.scheduleAtFixedRate(
- () -> {
- try {
- var expired =
- lobbyManager.findEmptyLobbiesOlderThan(
- Duration.ofSeconds(LOBBY_EXPIRY_SECONDS));
- for (var lid : expired) {
- // remove lobby from manager first
- lobbyManager.removeLobby(lid);
-
- // broadcast LOBBY_CLOSED event to all connected sessions
- // (requestId=0)
- for (Session s : sessionManager.getAllSessions()) {
- RequestContext ctx = new RequestContext(s.getId(), 0);
- SuccessResponse ev =
- new SuccessResponse(
- ctx,
- ResponseBody.builder()
- .param("EVENT", "LOBBY_CLOSED")
- .param("LOBBY_ID", lid.value())
- .build()) {};
-
- responseDispatcher.dispatch(ev);
- }
- }
- } catch (Exception e) {
- logger.warn("Lobby expiry job failed", e);
- }
- },
+ new LobbyCleanupJob(
+ lobbyManager,
+ sessionManager,
+ responseDispatcher,
+ Duration.ofSeconds(LOBBY_EXPIRY_SECONDS)),
LOBBY_CLEANUP_INITIAL_DELAY_SECONDS,
LOBBY_CLEANUP_PERIOD_SECONDS,
TimeUnit.SECONDS);
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyCleanupJob.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyCleanupJob.java
new file mode 100644
index 0000000..47c0413
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyCleanupJob.java
@@ -0,0 +1,100 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
+
+import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
+import java.time.Duration;
+import java.util.List;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/** Periodically removes expired empty lobbies and notifies connected sessions. */
+public class LobbyCleanupJob implements Runnable {
+
+ private static final Logger LOGGER = LogManager.getLogger(LobbyCleanupJob.class);
+
+ private final LobbyManager lobbyManager;
+ private final SessionManager sessionManager;
+ private final ResponseDispatcher responseDispatcher;
+ private final Duration expiryThreshold;
+
+ /**
+ * Creates a new cleanup job for expired empty lobbies.
+ *
+ * @param lobbyManager manager used to find and remove expired lobbies
+ * @param sessionManager manager used to resolve connected sessions
+ * @param responseDispatcher dispatcher used to notify clients about closed lobbies
+ * @param expiryThreshold age threshold that marks an empty lobby as expired
+ */
+ public LobbyCleanupJob(
+ LobbyManager lobbyManager,
+ SessionManager sessionManager,
+ ResponseDispatcher responseDispatcher,
+ Duration expiryThreshold) {
+ this.lobbyManager = lobbyManager;
+ this.sessionManager = sessionManager;
+ this.responseDispatcher = responseDispatcher;
+ this.expiryThreshold = expiryThreshold;
+ }
+
+ /**
+ * Runs one cleanup cycle.
+ *
+ *
The method first fetches all expired empty lobbies. Each lobby is then processed
+ * independently so that a failure for one lobby does not stop the remaining cleanups.
+ */
+ @Override
+ public void run() {
+ LOGGER.debug("Job started.");
+ try {
+ List expired;
+ try {
+ expired = lobbyManager.findEmptyLobbiesOlderThan(expiryThreshold);
+ } catch (Exception e) {
+ LOGGER.error("Lobby expiry job failed: could not fetch expired lobbies", e);
+ return;
+ }
+
+ for (LobbyId lobbyId : expired) {
+ try {
+ lobbyManager.removeLobby(lobbyId);
+ broadcastLobbyClosed(lobbyId);
+ } catch (RuntimeException e) {
+ LOGGER.warn("Failed to process expired lobby {}", lobbyId.value(), e);
+ }
+ }
+ } finally {
+ LOGGER.debug("Job finished.");
+ }
+ }
+
+ /**
+ * Broadcasts a lobby-closed event to all currently connected sessions.
+ *
+ * @param lobbyId id of the lobby that was closed
+ */
+ private void broadcastLobbyClosed(LobbyId lobbyId) {
+ for (Session session : sessionManager.getAllSessions()) {
+ try {
+ RequestContext ctx = new RequestContext(session.getId(), 0);
+ SuccessResponse event =
+ new SuccessResponse(
+ ctx,
+ ResponseBody.builder()
+ .param("EVENT", "LOBBY_CLOSED")
+ .param("LOBBY_ID", lobbyId.value())
+ .build()) {};
+ responseDispatcher.dispatch(event);
+ } catch (RuntimeException e) {
+ LOGGER.warn(
+ "Failed to notify session {} about closed lobby {}",
+ session.getId().value(),
+ lobbyId.value(),
+ e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifier.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifier.java
index 330dcbb..2545fc3 100644
--- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifier.java
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifier.java
@@ -48,7 +48,7 @@ public class TokenClassifier {
}
private static void validateSeparator(List rawTokens, int index) {
- boolean missingKey = index == 0 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
+ boolean missingKey = index <= 1 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
boolean missingValue =
index + 1 >= rawTokens.size()
diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifierTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifierTest.java
new file mode 100644
index 0000000..8c8748f
--- /dev/null
+++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifierTest.java
@@ -0,0 +1,94 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+public class TokenClassifierTest {
+ @Test
+ void testClassifySimpleCommand() {
+ List raw = Tokenizer.tokenize("PING");
+ List tokens = TokenClassifier.classify(raw);
+
+ assertEquals(2, tokens.size());
+ assertEquals(TokenType.COMMAND, tokens.get(0).type());
+ assertEquals("PING", tokens.get(0).value());
+ assertEquals(TokenType.EOF, tokens.get(1).type());
+ }
+
+ @Test
+ void testCommandWithParameter() {
+ List raw = Tokenizer.tokenize("ANSWER VALUE=42");
+ List tokens = TokenClassifier.classify(raw);
+
+ assertEquals(5, tokens.size());
+ assertEquals(TokenType.COMMAND, tokens.get(0).type());
+ assertEquals(TokenType.KEY, tokens.get(1).type());
+ assertEquals(TokenType.SEPARATOR, tokens.get(2).type());
+ assertEquals(TokenType.VALUE, tokens.get(3).type());
+ assertEquals("42", tokens.get(3).value());
+ assertEquals(TokenType.EOF, tokens.get(4).type());
+ }
+
+ @Test
+ void testStringValue() {
+ List raw = Tokenizer.tokenize("GREET MSG='Hello World'");
+ List tokens = TokenClassifier.classify(raw);
+
+ assertEquals(5, tokens.size());
+ assertEquals(TokenType.COMMAND, tokens.get(0).type());
+ assertEquals(TokenType.KEY, tokens.get(1).type());
+ assertEquals(TokenType.SEPARATOR, tokens.get(2).type());
+ assertEquals(TokenType.VALUE, tokens.get(3).type());
+ assertEquals("Hello World", tokens.get(3).value());
+ }
+
+ @Test
+ void testUnexpectedStringLiteralThrows() {
+ List raw = Tokenizer.tokenize("CMD 'oops'");
+
+ TokenizerException ex =
+ assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
+ assertTrue(ex.getMessage().contains("Unexpected string literal"));
+ }
+
+ @Test
+ void testMissingValueAfterSeparatorThrows() {
+ List raw = Tokenizer.tokenize("CMD KEY=");
+
+ TokenizerException ex =
+ assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
+ assertEquals("Expected VALUE after '='", ex.getMessage());
+ }
+
+ @Test
+ void testMissingKeyBeforeSeparatorThrows() {
+ List raw = Tokenizer.tokenize("CMD =VALUE");
+
+ TokenizerException ex =
+ assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
+ assertEquals("Expected KEY before '='", ex.getMessage());
+ }
+
+ @Test
+ void testNextWordIsKeyThrows() {
+ List raw = Tokenizer.tokenize("CMD KEY1=KEY2=42");
+
+ TokenizerException ex =
+ assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
+ assertEquals("Expected VALUE after '='", ex.getMessage());
+ }
+
+ @Test
+ void testEmptyRawTokensThrows() {
+ List raw = new ArrayList<>();
+
+ TokenizerException ex =
+ assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
+ assertEquals("Expected COMMAND as first token", ex.getMessage());
+ assertEquals(1, ex.getLine());
+ assertEquals(1, ex.getColumn());
+ }
+}
diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerTest.java
new file mode 100644
index 0000000..bd5e486
--- /dev/null
+++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerTest.java
@@ -0,0 +1,211 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class TokenizerTest {
+ @Test
+ void testEofAlwaysPresent() {
+ List tokens = Tokenizer.tokenize("");
+
+ assertEquals(1, tokens.size());
+ assertEquals(RawTokenType.EOF, tokens.get(0).type());
+ }
+
+ @Test
+ void testSimpleCommand() {
+ List tokens = Tokenizer.tokenize("PING");
+ assertEquals(2, tokens.size());
+ assertEquals(RawTokenType.WORD, tokens.get(0).type());
+ assertEquals("PING", tokens.get(0).value());
+ assertEquals(RawTokenType.EOF, tokens.get(1).type());
+ }
+
+ @Test
+ void testSimpleCommandWithUnderscore() {
+ List tokens = Tokenizer.tokenize("JOIN_GAME");
+ assertEquals(2, tokens.size());
+ assertEquals(RawTokenType.WORD, tokens.get(0).type());
+ assertEquals("JOIN_GAME", tokens.get(0).value());
+ assertEquals(RawTokenType.EOF, tokens.get(1).type());
+ }
+
+ @Test
+ void testCommandWithOneParameter() {
+ List tokens = Tokenizer.tokenize("GET VALUE=42");
+ assertEquals(5, tokens.size());
+
+ assertEquals(RawTokenType.WORD, tokens.get(0).type());
+ assertEquals("GET", tokens.get(0).value());
+
+ // Parameter
+ assertEquals(RawTokenType.WORD, tokens.get(1).type());
+ assertEquals("VALUE", tokens.get(1).value());
+
+ assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
+ assertEquals("=", tokens.get(2).value());
+
+ assertEquals(RawTokenType.WORD, tokens.get(3).type());
+ assertEquals("42", tokens.get(3).value());
+
+ assertEquals(RawTokenType.EOF, tokens.get(4).type());
+ }
+
+ @Test
+ void testCommandWithOneParameterWhitespacesBetweenSeperator() {
+ List tokens = Tokenizer.tokenize("GET VALUE = 42");
+ assertEquals(5, tokens.size());
+
+ assertEquals(RawTokenType.WORD, tokens.get(0).type());
+ assertEquals("GET", tokens.get(0).value());
+
+ // Parameter
+ assertEquals(RawTokenType.WORD, tokens.get(1).type());
+ assertEquals("VALUE", tokens.get(1).value());
+
+ assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
+ assertEquals("=", tokens.get(2).value());
+
+ assertEquals(RawTokenType.WORD, tokens.get(3).type());
+ assertEquals("42", tokens.get(3).value());
+
+ assertEquals(RawTokenType.EOF, tokens.get(4).type());
+ }
+
+ @Test
+ void testCommandWithOneParameterAndNewline() {
+ List tokens = Tokenizer.tokenize("GET\nVALUE=42");
+ assertEquals(6, tokens.size());
+
+ assertEquals(RawTokenType.WORD, tokens.get(0).type());
+ assertEquals("GET", tokens.get(0).value());
+
+ // Parameter
+ assertEquals(RawTokenType.WORD, tokens.get(2).type());
+ assertEquals("VALUE", tokens.get(2).value());
+
+ assertEquals(RawTokenType.SEPARATOR, tokens.get(3).type());
+ assertEquals("=", tokens.get(3).value());
+
+ assertEquals(RawTokenType.WORD, tokens.get(4).type());
+ assertEquals("42", tokens.get(4).value());
+
+ assertEquals(RawTokenType.EOF, tokens.get(5).type());
+ }
+
+ @Test
+ void testCommandWithStringParameter() {
+ List tokens = Tokenizer.tokenize("GREET MSG='Hello World'");
+ assertEquals(5, tokens.size());
+
+ assertEquals(RawTokenType.WORD, tokens.get(0).type());
+ assertEquals("GREET", tokens.get(0).value());
+
+ // First parameter
+ assertEquals(RawTokenType.WORD, tokens.get(1).type());
+ assertEquals("MSG", tokens.get(1).value());
+
+ assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
+ assertEquals("=", tokens.get(2).value());
+
+ assertEquals(RawTokenType.STRING, tokens.get(3).type());
+ assertEquals("Hello World", tokens.get(3).value());
+
+ assertEquals(RawTokenType.EOF, tokens.get(4).type());
+ }
+
+ @Test
+ void testCommandWithMultilineStringParameter() {
+ List tokens = Tokenizer.tokenize("GREET MSG='Hello\nWorld'");
+ assertEquals(5, tokens.size());
+
+ assertEquals(RawTokenType.WORD, tokens.get(0).type());
+ assertEquals("GREET", tokens.get(0).value());
+
+ // First parameter
+ assertEquals(RawTokenType.WORD, tokens.get(1).type());
+ assertEquals("MSG", tokens.get(1).value());
+
+ assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
+ assertEquals("=", tokens.get(2).value());
+
+ assertEquals(RawTokenType.STRING, tokens.get(3).type());
+ assertEquals("Hello\nWorld", tokens.get(3).value());
+
+ assertEquals(RawTokenType.EOF, tokens.get(4).type());
+ }
+
+ @Test
+ void testWhitespace() {
+ List tokens = Tokenizer.tokenize(" \t PING");
+ assertEquals(2, tokens.size());
+ assertEquals(RawTokenType.WORD, tokens.get(0).type());
+ assertEquals("PING", tokens.get(0).value());
+ }
+
+ @Test
+ void testStringWithEscapedQuote() {
+ List tokens =
+ Tokenizer.tokenize("WONDERFUL_GREETING MSG='it\\'s a wonderful day'");
+ assertEquals(5, tokens.size());
+ assertEquals(RawTokenType.STRING, tokens.get(3).type());
+ assertEquals("it's a wonderful day", tokens.get(3).value());
+ }
+
+ @Test
+ void testStringWithWronglyEscapedQuote() {
+ TokenizerException ex =
+ assertThrows(
+ TokenizerException.class,
+ () -> Tokenizer.tokenize("WONDERFUL_GREETING MSG='it\'s a wonderful day'"));
+ assertEquals("Unterminated string literal", ex.getMessage());
+ }
+
+ @Test
+ void testStringWithEscapedBackslash() {
+ List tokens = Tokenizer.tokenize("EXECUTE_COMMAND CMD='\\whoami'");
+ assertEquals(5, tokens.size());
+ assertEquals(RawTokenType.STRING, tokens.get(3).type());
+ assertEquals("\\whoami", tokens.get(3).value());
+ }
+
+ @Test
+ void testColumnTracking() {
+ List tokens = Tokenizer.tokenize("GET VAL=1");
+ assertEquals(5, tokens.size());
+ assertEquals(1, tokens.get(0).column());
+ assertEquals(5, tokens.get(1).column());
+ assertEquals(8, tokens.get(2).column());
+ assertEquals(9, tokens.get(3).column());
+ }
+
+ @Test
+ void testUnterminatedStringThrows() {
+ TokenizerException ex =
+ assertThrows(
+ TokenizerException.class, () -> Tokenizer.tokenize("GREETING = 'unclosed"));
+ assertEquals(1, ex.getLine());
+ assertEquals(12, ex.getColumn());
+ assertTrue(ex.getMessage().contains("Unterminated string literal"));
+ }
+
+ @Test
+ void testUnexpectedCharacterThrows() {
+ TokenizerException ex =
+ assertThrows(TokenizerException.class, () -> Tokenizer.tokenize("CMD @ KEY=VALUE"));
+ assertTrue(ex.getMessage().contains("Unexpected character '@'"));
+ assertEquals(1, ex.getLine());
+ }
+
+ @Test
+ void testEmptyStringValue() {
+ List tokens = Tokenizer.tokenize("cmd = ''");
+ assertEquals(4, tokens.size());
+ assertEquals(RawTokenType.STRING, tokens.get(2).type());
+ assertEquals("", tokens.get(2).value());
+ }
+}
diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/RequestParameterAccessorTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/RequestParameterAccessorTest.java
new file mode 100644
index 0000000..e290293
--- /dev/null
+++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/RequestParameterAccessorTest.java
@@ -0,0 +1,115 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+public class RequestParameterAccessorTest {
+ @Test
+ void testRequiredParameterPresent() {
+ List parameters = List.of(new RequestParameter("ARG1", "VAL1"));
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ assertEquals("VAL1", accessor.require("ARG1"));
+ }
+
+ @Test
+ void testMissingRequiredParameterThrows() {
+ List parameters = List.of();
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ MissingParameterException ex =
+ assertThrows(MissingParameterException.class, () -> accessor.require("ARG1"));
+ assertEquals("Required parameter with key 'ARG1' is missing.", ex.getMessage());
+ assertEquals("ARG1", ex.getParameterKey());
+ }
+
+ @Test
+ void testRequiredParameterSuccessfulParsing() {
+ List parameters = List.of(new RequestParameter("ARG1", "42"));
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ assertEquals(42, (int) accessor.require("ARG1", Integer::valueOf));
+ }
+
+ @Test
+ void testRequiredParameterInvalidParsingThrows() {
+ List parameters =
+ List.of(new RequestParameter("ARG1", "The answer is: 42"));
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ ParameterParseException ex =
+ assertThrows(
+ ParameterParseException.class,
+ () -> accessor.require("ARG1", Integer::valueOf));
+ assertEquals("Error while parsing 'ARG1' with specified parser", ex.getMessage());
+ assertEquals("ARG1", ex.getParameterKey());
+ }
+
+ @Test
+ void testOptionalParameterPresent() {
+ List parameters = List.of(new RequestParameter("ARG1", "VAL1"));
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ assertEquals("VAL1", accessor.optional("ARG1", "DEFAULT"));
+ }
+
+ @Test
+ void testOptionalParameterMissing() {
+ List parameters = List.of();
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ assertEquals("DEFAULT", accessor.optional("ARG1", "DEFAULT"));
+ }
+
+ @Test
+ void testOptionalParameterSuccessfulParsing() {
+ List parameters = List.of(new RequestParameter("ARG1", "42"));
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ assertEquals(42, (int) accessor.optional("ARG1", 7411, Integer::valueOf));
+ }
+
+ @Test
+ void testMissingOptionalParameterSuccessfulParsing() {
+ List parameters = List.of();
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ assertEquals(7411, (int) accessor.optional("ARG1", 7411, Integer::valueOf));
+ }
+
+ @Test
+ void testOptionalParameterInvalidParsingThrows() {
+ List parameters =
+ List.of(new RequestParameter("ARG1", "The answer is: 42"));
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ ParameterParseException ex =
+ assertThrows(
+ ParameterParseException.class,
+ () -> accessor.optional("ARG1", 7411, Integer::valueOf));
+ assertEquals("Error while parsing 'ARG1' with specified parser", ex.getMessage());
+ assertEquals("ARG1", ex.getParameterKey());
+ }
+
+ @Test
+ void testContainsExistingKey() {
+ List parameters = List.of(new RequestParameter("ARG1", "VAL1"));
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ assertTrue(accessor.containsKey("ARG1"));
+ }
+
+ @Test
+ void testContainsMissingKey() {
+ List parameters = List.of();
+ RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
+
+ assertFalse(accessor.containsKey("ARG1"));
+ }
+}