From 69ce5a6dda60bc315a78d7d78d4c0d87a830e067 Mon Sep 17 00:00:00 2001
From: ferid333 <135500346+ferid333@users.noreply.github.com>
Date: Mon, 3 Aug 2026 19:23:16 +1000
Subject: [PATCH] Phase-tech: Adding tests and checkstyle for the project
---
build.gradle | 7 +
config/checkstyle/checkstyle.xml | 18 ++
src/main/java/org/cache/Main.java | 14 +-
.../java/org/cache/client/CacheClient.java | 7 +
.../java/org/cache/client/TcpCacheClient.java | 35 ++++
.../org/cache/eviction/LruEvictionPolicy.java | 6 +-
.../org/cache/eviction/MruEvictionPolicy.java | 6 +-
.../ClientConnectionHandlerFactory.java | 9 +
.../org/cache/network/TcpCacheServer.java | 26 ++-
.../network/connection/RespConnection.java | 9 +-
.../org/cache/protocol/CommandParser.java | 13 +-
.../cache/protocol/codec/ListValueCodec.java | 6 +-
.../protocol/commands/LrangeCommand.java | 4 +-
.../cache/protocol/commands/PutCommand.java | 2 -
.../org/cache/client/TcpCacheClientTest.java | 173 ++++++++++++++++++
.../java/org/cache/core/LocalCacheTest.java | 138 ++++++++++++++
.../cache/eviction/LruEvictionPolicyTest.java | 44 +++++
.../cache/eviction/MruEvictionPolicyTest.java | 44 +++++
.../org/cache/network/TcpCacheServerTest.java | 34 ++++
.../AutoProtocolConnectionTest.java | 64 +++++++
.../ClientConnectionHandlerTest.java | 36 ++++
.../LineProtocolConnectionTest.java | 58 ++++++
.../connection/RespConnectionTest.java | 71 +++++++
.../org/cache/protocol/CommandParserTest.java | 134 ++++++++++++++
.../cache/protocol/CommandProcessorTest.java | 35 ++++
.../protocol/codec/IntegerKeyCodecTest.java | 34 ++++
.../protocol/codec/ListValueCodecTest.java | 37 ++++
.../protocol/codec/StringKeyCodecTest.java | 27 +++
.../protocol/codec/StringValueCodecTest.java | 34 ++++
.../codec/ValueCodecRegistryTest.java | 36 ++++
.../protocol/commands/ClearCommandTest.java | 35 ++++
.../protocol/commands/DeleteCommandTest.java | 23 +++
.../protocol/commands/GetCommandTest.java | 59 ++++++
.../protocol/commands/InvalidCommandTest.java | 20 ++
.../protocol/commands/LrangeCommandTest.java | 96 ++++++++++
.../protocol/commands/MetricsCommandTest.java | 24 +++
.../protocol/commands/PushCommandTest.java | 59 ++++++
.../protocol/commands/PutCommandTest.java | 33 ++++
.../protocol/commands/SizeCommandTest.java | 23 +++
.../protocol/commands/UnknownCommandTest.java | 20 ++
40 files changed, 1527 insertions(+), 26 deletions(-)
create mode 100644 config/checkstyle/checkstyle.xml
create mode 100644 src/main/java/org/cache/network/ClientConnectionHandlerFactory.java
create mode 100644 src/test/java/org/cache/client/TcpCacheClientTest.java
create mode 100644 src/test/java/org/cache/core/LocalCacheTest.java
create mode 100644 src/test/java/org/cache/eviction/LruEvictionPolicyTest.java
create mode 100644 src/test/java/org/cache/eviction/MruEvictionPolicyTest.java
create mode 100644 src/test/java/org/cache/network/TcpCacheServerTest.java
create mode 100644 src/test/java/org/cache/network/connection/AutoProtocolConnectionTest.java
create mode 100644 src/test/java/org/cache/network/connection/ClientConnectionHandlerTest.java
create mode 100644 src/test/java/org/cache/network/connection/LineProtocolConnectionTest.java
create mode 100644 src/test/java/org/cache/network/connection/RespConnectionTest.java
create mode 100644 src/test/java/org/cache/protocol/CommandParserTest.java
create mode 100644 src/test/java/org/cache/protocol/CommandProcessorTest.java
create mode 100644 src/test/java/org/cache/protocol/codec/IntegerKeyCodecTest.java
create mode 100644 src/test/java/org/cache/protocol/codec/ListValueCodecTest.java
create mode 100644 src/test/java/org/cache/protocol/codec/StringKeyCodecTest.java
create mode 100644 src/test/java/org/cache/protocol/codec/StringValueCodecTest.java
create mode 100644 src/test/java/org/cache/protocol/codec/ValueCodecRegistryTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/ClearCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/DeleteCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/GetCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/InvalidCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/LrangeCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/MetricsCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/PushCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/PutCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/SizeCommandTest.java
create mode 100644 src/test/java/org/cache/protocol/commands/UnknownCommandTest.java
diff --git a/build.gradle b/build.gradle
index eb4ae53..2050cc9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,7 @@
plugins {
id 'java'
id 'application'
+ id 'checkstyle'
}
group = 'org.example'
@@ -13,6 +14,7 @@ repositories {
dependencies {
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
+ testImplementation 'org.mockito:mockito-junit-jupiter:5.12.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
@@ -26,4 +28,9 @@ tasks.withType(JavaCompile).configureEach {
test {
useJUnitPlatform()
+ systemProperty 'net.bytebuddy.experimental', 'true'
+}
+
+checkstyle {
+ configFile = file('config/checkstyle/checkstyle.xml')
}
\ No newline at end of file
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000..0de7181
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/org/cache/Main.java b/src/main/java/org/cache/Main.java
index e066ed6..706018c 100644
--- a/src/main/java/org/cache/Main.java
+++ b/src/main/java/org/cache/Main.java
@@ -3,6 +3,7 @@
import org.cache.core.LocalCache;
import org.cache.core.ValueType;
import org.cache.eviction.LruEvictionPolicy;
+import org.cache.network.connection.ClientConnectionHandler;
import org.cache.network.TcpCacheServer;
import org.cache.protocol.CommandParser;
import org.cache.protocol.CommandProcessor;
@@ -12,8 +13,12 @@
import org.cache.protocol.codec.ValueCodecRegistry;
import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.concurrent.Executors;
public class Main {
+ private static final int SERVER_THREAD_COUNT = 16;
+
public static void main(String[] args) throws IOException {
int port = args.length > 0 ? Integer.parseInt(args[0]) : 2020;
var cache = new LocalCache(1_000, new LruEvictionPolicy<>());
@@ -24,7 +29,14 @@ public static void main(String[] args) throws IOException {
var commandProcessor = new CommandProcessor<>(cache, commandParser, valueCodecs);
- try (cache; var server = new TcpCacheServer(port, commandProcessor)) {
+ try (cache;
+ var serverSocket = new ServerSocket(port);
+ var executor = Executors.newFixedThreadPool(SERVER_THREAD_COUNT);
+ var server = new TcpCacheServer(
+ serverSocket,
+ executor,
+ socket -> new ClientConnectionHandler(socket, commandProcessor)
+ )) {
server.start();
}
}
diff --git a/src/main/java/org/cache/client/CacheClient.java b/src/main/java/org/cache/client/CacheClient.java
index 8912bae..3d59459 100644
--- a/src/main/java/org/cache/client/CacheClient.java
+++ b/src/main/java/org/cache/client/CacheClient.java
@@ -2,6 +2,7 @@
import org.cache.core.metrics.Snapshot;
+import java.util.List;
import java.util.Optional;
public interface CacheClient {
@@ -19,4 +20,10 @@ public interface CacheClient {
int size();
void clear();
+
+ void push(K key, V value);
+
+ List lrange(K key, int to);
+
+ List lrange(K key, int from, int to);
}
diff --git a/src/main/java/org/cache/client/TcpCacheClient.java b/src/main/java/org/cache/client/TcpCacheClient.java
index 775f064..ac3d584 100644
--- a/src/main/java/org/cache/client/TcpCacheClient.java
+++ b/src/main/java/org/cache/client/TcpCacheClient.java
@@ -24,6 +24,7 @@ public class TcpCacheClient implements CacheClient, AutoCloseable {
private static final int FIRST_VALUE_INDEX = 1;
private static final int METRIC_NAME_VALUE_START_INDEX = 1;
private static final int METRIC_NAME_VALUE_PAIR_SIZE = 2;
+ private static final int DEFAULT_LRANGE_FROM_INDEX = 0;
private final RespConnection connection;
private final KeyCodec keyCodec;
@@ -45,6 +46,12 @@ public TcpCacheClient(
}
}
+ TcpCacheClient(RespConnection connection, KeyCodec keyCodec, Serializer valueSerializer) {
+ this.connection = connection;
+ this.keyCodec = keyCodec;
+ this.valueSerializer = valueSerializer;
+ }
+
@Override
public void put(K key, V value, long ttl) {
expectOk(List.of("PUT", keyCodec.encode(key), valueSerializer.encode(value), Long.toString(ttl)));
@@ -127,6 +134,34 @@ public void clear() {
expectOk(List.of("CLEAR"));
}
+ @Override
+ public void push(K key, V value) {
+ expectOk(List.of("PUSH", keyCodec.encode(key), valueSerializer.encode(value)));
+ }
+
+ @Override
+ public List lrange(K key, int to) {
+ return lrange(key, DEFAULT_LRANGE_FROM_INDEX, to);
+ }
+
+ @Override
+ public List lrange(K key, int from, int to) {
+ List response = send(List.of(
+ "LRANGE",
+ keyCodec.encode(key),
+ Integer.toString(from),
+ Integer.toString(to)
+ ));
+
+ if (isResponse(response, ResponseConstants.NOT_FOUND.name(), SINGLE_PART_RESPONSE_SIZE)) {
+ return List.of();
+ }
+
+ return response.stream()
+ .map(valueSerializer::decode)
+ .toList();
+ }
+
@Override
public void close() {
try {
diff --git a/src/main/java/org/cache/eviction/LruEvictionPolicy.java b/src/main/java/org/cache/eviction/LruEvictionPolicy.java
index d4b349d..cfccd9a 100644
--- a/src/main/java/org/cache/eviction/LruEvictionPolicy.java
+++ b/src/main/java/org/cache/eviction/LruEvictionPolicy.java
@@ -46,7 +46,9 @@ public synchronized void onKeyAccessed(K key) {
@Override
public synchronized void onKeyRemoved(K key) {
var removedNode = keyMap.get(key);
- if (removedNode == null) return;
+ if (removedNode == null) {
+ return;
+ }
var prevNode = removedNode.prev;
var nextNode = removedNode.next;
@@ -67,4 +69,4 @@ public synchronized Optional selectVictim() {
return Optional.ofNullable(selectedNode.value);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/cache/eviction/MruEvictionPolicy.java b/src/main/java/org/cache/eviction/MruEvictionPolicy.java
index 25af74b..7c475cb 100644
--- a/src/main/java/org/cache/eviction/MruEvictionPolicy.java
+++ b/src/main/java/org/cache/eviction/MruEvictionPolicy.java
@@ -46,7 +46,9 @@ public synchronized void onKeyAccessed(K key) {
@Override
public synchronized void onKeyRemoved(K key) {
var removedNode = keyMap.get(key);
- if (removedNode == null) return;
+ if (removedNode == null) {
+ return;
+ }
var prevNode = removedNode.prev;
var nextNode = removedNode.next;
@@ -67,4 +69,4 @@ public synchronized Optional selectVictim() {
return Optional.ofNullable(selectedNode.value);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/cache/network/ClientConnectionHandlerFactory.java b/src/main/java/org/cache/network/ClientConnectionHandlerFactory.java
new file mode 100644
index 0000000..7d38654
--- /dev/null
+++ b/src/main/java/org/cache/network/ClientConnectionHandlerFactory.java
@@ -0,0 +1,9 @@
+package org.cache.network;
+
+import java.net.Socket;
+
+@FunctionalInterface
+public interface ClientConnectionHandlerFactory {
+
+ Runnable create(Socket socket);
+}
diff --git a/src/main/java/org/cache/network/TcpCacheServer.java b/src/main/java/org/cache/network/TcpCacheServer.java
index 8609ebe..5a00aa8 100644
--- a/src/main/java/org/cache/network/TcpCacheServer.java
+++ b/src/main/java/org/cache/network/TcpCacheServer.java
@@ -1,36 +1,32 @@
package org.cache.network;
-import org.cache.network.connection.ClientConnectionHandler;
-import org.cache.protocol.CommandProcessor;
-
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
public class TcpCacheServer implements AutoCloseable {
- private final int port;
- private final CommandProcessor> commandProcessor;
private final ExecutorService executor;
+ private final ClientConnectionHandlerFactory handlerFactory;
private ServerSocket serverSocket;
- private static final int THREAD_COUNT = 16;
-
- public TcpCacheServer(int port, CommandProcessor> commandProcessor) {
- this.port = port;
- this.commandProcessor = commandProcessor;
- this.executor = Executors.newFixedThreadPool(THREAD_COUNT);
+ public TcpCacheServer(
+ ServerSocket serverSocket,
+ ExecutorService executor,
+ ClientConnectionHandlerFactory handlerFactory
+ ) {
+ this.serverSocket = serverSocket;
+ this.executor = executor;
+ this.handlerFactory = handlerFactory;
}
public void start() throws IOException {
- serverSocket = new ServerSocket(port);
- System.out.println("TCP cache server listening on port " + port);
+ System.out.println("TCP cache server listening on port " + serverSocket.getLocalPort());
while (!Thread.currentThread().isInterrupted() && !serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
- executor.submit(new ClientConnectionHandler(socket, commandProcessor));
+ executor.submit(handlerFactory.create(socket));
}
}
diff --git a/src/main/java/org/cache/network/connection/RespConnection.java b/src/main/java/org/cache/network/connection/RespConnection.java
index 410edd0..57e6004 100644
--- a/src/main/java/org/cache/network/connection/RespConnection.java
+++ b/src/main/java/org/cache/network/connection/RespConnection.java
@@ -11,7 +11,14 @@
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.cache.protocol.ProtocolConstants.*;
+import static org.cache.protocol.ProtocolConstants.ARRAY_PREFIX;
+import static org.cache.protocol.ProtocolConstants.BULK_STRING_PREFIX;
+import static org.cache.protocol.ProtocolConstants.CARRIAGE_RETURN;
+import static org.cache.protocol.ProtocolConstants.CRLF;
+import static org.cache.protocol.ProtocolConstants.ERROR_PREFIX;
+import static org.cache.protocol.ProtocolConstants.INTEGER_PREFIX;
+import static org.cache.protocol.ProtocolConstants.SIMPLE_STRING_PREFIX;
+import static org.cache.protocol.ProtocolConstants.NEW_LINE;
import static org.cache.protocol.RegexConstants.COMMA_WITH_OPTIONAL_WHITESPACE;
import static org.cache.protocol.RegexConstants.KEY_VALUE_SEPARATOR;
import static org.cache.protocol.RegexConstants.SPACE;
diff --git a/src/main/java/org/cache/protocol/CommandParser.java b/src/main/java/org/cache/protocol/CommandParser.java
index af642b6..78be5ff 100644
--- a/src/main/java/org/cache/protocol/CommandParser.java
+++ b/src/main/java/org/cache/protocol/CommandParser.java
@@ -2,7 +2,18 @@
import org.cache.core.ValueType;
import org.cache.protocol.codec.KeyCodec;
-import org.cache.protocol.commands.*;
+import org.cache.protocol.commands.CacheCommand;
+import org.cache.protocol.commands.ClearCommand;
+import org.cache.protocol.commands.CommandType;
+import org.cache.protocol.commands.DeleteCommand;
+import org.cache.protocol.commands.GetCommand;
+import org.cache.protocol.commands.InvalidCommand;
+import org.cache.protocol.commands.LrangeCommand;
+import org.cache.protocol.commands.MetricsCommand;
+import org.cache.protocol.commands.PushCommand;
+import org.cache.protocol.commands.PutCommand;
+import org.cache.protocol.commands.SizeCommand;
+import org.cache.protocol.commands.UnknownCommand;
import java.util.List;
diff --git a/src/main/java/org/cache/protocol/codec/ListValueCodec.java b/src/main/java/org/cache/protocol/codec/ListValueCodec.java
index dffb5b9..015ddbd 100644
--- a/src/main/java/org/cache/protocol/codec/ListValueCodec.java
+++ b/src/main/java/org/cache/protocol/codec/ListValueCodec.java
@@ -1,6 +1,10 @@
package org.cache.protocol.codec;
-import java.io.*;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/main/java/org/cache/protocol/commands/LrangeCommand.java b/src/main/java/org/cache/protocol/commands/LrangeCommand.java
index 8de0c0f..7ccf8a0 100644
--- a/src/main/java/org/cache/protocol/commands/LrangeCommand.java
+++ b/src/main/java/org/cache/protocol/commands/LrangeCommand.java
@@ -9,7 +9,9 @@
import java.util.List;
import java.util.Optional;
-import static org.cache.protocol.commands.ResponseConstants.*;
+import static org.cache.protocol.commands.ResponseConstants.ERROR;
+import static org.cache.protocol.commands.ResponseConstants.LIST;
+import static org.cache.protocol.commands.ResponseConstants.NOT_FOUND;
public class LrangeCommand implements CacheCommand {
diff --git a/src/main/java/org/cache/protocol/commands/PutCommand.java b/src/main/java/org/cache/protocol/commands/PutCommand.java
index b689f47..2d7b6de 100644
--- a/src/main/java/org/cache/protocol/commands/PutCommand.java
+++ b/src/main/java/org/cache/protocol/commands/PutCommand.java
@@ -5,8 +5,6 @@
import org.cache.protocol.codec.ValueCodec;
import org.cache.protocol.codec.ValueCodecRegistry;
-import java.util.Arrays;
-
import static org.cache.protocol.commands.ResponseConstants.OK;
public class PutCommand implements CacheCommand {
diff --git a/src/test/java/org/cache/client/TcpCacheClientTest.java b/src/test/java/org/cache/client/TcpCacheClientTest.java
new file mode 100644
index 0000000..3a8bc44
--- /dev/null
+++ b/src/test/java/org/cache/client/TcpCacheClientTest.java
@@ -0,0 +1,173 @@
+package org.cache.client;
+
+import org.cache.client.serializer.StringSerializer;
+import org.cache.core.metrics.Snapshot;
+import org.cache.network.connection.RespConnection;
+import org.cache.protocol.codec.StringKeyCodec;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class TcpCacheClientTest {
+
+ private RespConnection connection;
+ private TcpCacheClient client;
+
+ @BeforeEach
+ void setUp() {
+ connection = mock(RespConnection.class);
+ client = new TcpCacheClient<>(connection, new StringKeyCodec(), new StringSerializer());
+ }
+
+ @Test
+ void putSendsPutCommand() throws Exception {
+ when(connection.sendCommandForResponse(List.of("PUT", "fruit", "apple"))).thenReturn(List.of("OK"));
+
+ client.put("fruit", "apple");
+
+ verify(connection).sendCommandForResponse(List.of("PUT", "fruit", "apple"));
+ }
+
+ @Test
+ void putWithTtlSendsPutCommandWithTtl() throws Exception {
+ when(connection.sendCommandForResponse(List.of("PUT", "fruit", "apple", "1000"))).thenReturn(List.of("OK"));
+
+ client.put("fruit", "apple", 1_000);
+
+ verify(connection).sendCommandForResponse(List.of("PUT", "fruit", "apple", "1000"));
+ }
+
+ @Test
+ void getReturnsValueWhenServerReturnsValue() throws Exception {
+ when(connection.sendCommandForResponse(List.of("GET", "fruit"))).thenReturn(List.of("VALUE", "apple"));
+
+ Optional value = client.get("fruit");
+
+ assertEquals(Optional.of("apple"), value);
+ }
+
+ @Test
+ void getReturnsEmptyWhenServerReturnsNotFound() throws Exception {
+ when(connection.sendCommandForResponse(List.of("GET", "missing"))).thenReturn(List.of("NOT_FOUND"));
+
+ Optional value = client.get("missing");
+
+ assertEquals(Optional.empty(), value);
+ }
+
+ @Test
+ void deleteSendsDeleteCommand() throws Exception {
+ when(connection.sendCommandForResponse(List.of("DELETE", "fruit"))).thenReturn(List.of("OK"));
+
+ client.delete("fruit");
+
+ verify(connection).sendCommandForResponse(List.of("DELETE", "fruit"));
+ }
+
+ @Test
+ void sizeReturnsParsedSize() throws Exception {
+ when(connection.sendCommandForResponse(List.of("SIZE"))).thenReturn(List.of("SIZE", "3"));
+
+ int size = client.size();
+
+ assertEquals(3, size);
+ }
+
+ @Test
+ void clearSendsClearCommand() throws Exception {
+ when(connection.sendCommandForResponse(List.of("CLEAR"))).thenReturn(List.of("OK"));
+
+ client.clear();
+
+ verify(connection).sendCommandForResponse(List.of("CLEAR"));
+ }
+
+ @Test
+ void pushSendsPushCommand() throws Exception {
+ when(connection.sendCommandForResponse(List.of("PUSH", "fruits", "apple"))).thenReturn(List.of("OK"));
+
+ client.push("fruits", "apple");
+
+ verify(connection).sendCommandForResponse(List.of("PUSH", "fruits", "apple"));
+ }
+
+ @Test
+ void lrangeSendsRangeCommandAndReturnsValues() throws Exception {
+ when(connection.sendCommandForResponse(List.of("LRANGE", "fruits", "0", "2")))
+ .thenReturn(List.of("apple", "banana"));
+
+ List values = client.lrange("fruits", 0, 2);
+
+ assertEquals(List.of("apple", "banana"), values);
+ }
+
+ @Test
+ void lrangeWithToUsesZeroAsFromIndex() throws Exception {
+ when(connection.sendCommandForResponse(List.of("LRANGE", "fruits", "0", "2")))
+ .thenReturn(List.of("apple", "banana"));
+
+ List values = client.lrange("fruits", 2);
+
+ assertEquals(List.of("apple", "banana"), values);
+ }
+
+ @Test
+ void lrangeReturnsEmptyListWhenServerReturnsNotFound() throws Exception {
+ when(connection.sendCommandForResponse(List.of("LRANGE", "missing", "0", "2")))
+ .thenReturn(List.of("NOT_FOUND"));
+
+ List values = client.lrange("missing", 0, 2);
+
+ assertEquals(List.of(), values);
+ }
+
+ @Test
+ void metricsReturnsParsedSnapshot() throws Exception {
+ when(connection.sendCommandForResponse(List.of("METRICS"))).thenReturn(List.of(
+ "METRICS",
+ "hits", "1",
+ "misses", "2",
+ "evictions", "3",
+ "expirations", "4",
+ "hitRate", "0.5"
+ ));
+
+ Snapshot metrics = client.metrics();
+
+ assertEquals(1, metrics.getHits());
+ assertEquals(2, metrics.getMisses());
+ assertEquals(3, metrics.getEvictions());
+ assertEquals(4, metrics.getExpirations());
+ assertEquals(0.5, metrics.getHitRate());
+ }
+
+ @Test
+ void commandThrowsWhenServerReturnsError() throws Exception {
+ when(connection.sendCommandForResponse(List.of("GET", "fruit"))).thenReturn(List.of("ERROR", "broken"));
+
+ assertThrows(CacheClientException.class, () -> client.get("fruit"));
+ }
+
+ @Test
+ void commandThrowsWhenConnectionFails() throws Exception {
+ when(connection.sendCommandForResponse(List.of("GET", "fruit"))).thenThrow(new IOException("closed"));
+
+ assertThrows(CacheClientException.class, () -> client.get("fruit"));
+ }
+
+ @Test
+ void closeClosesConnection() throws Exception {
+ client.close();
+
+ verify(connection).close();
+ }
+}
diff --git a/src/test/java/org/cache/core/LocalCacheTest.java b/src/test/java/org/cache/core/LocalCacheTest.java
new file mode 100644
index 0000000..4c55cb7
--- /dev/null
+++ b/src/test/java/org/cache/core/LocalCacheTest.java
@@ -0,0 +1,138 @@
+package org.cache.core;
+
+import org.cache.eviction.LruEvictionPolicy;
+import org.cache.core.metrics.Snapshot;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class LocalCacheTest {
+
+ private static final int DEFAULT_CAPACITY = 10;
+
+ private LocalCache cache;
+
+ @BeforeEach
+ void setUp() {
+ cache = createCache(DEFAULT_CAPACITY);
+ }
+
+ @AfterEach
+ void tearDown() {
+ cache.close();
+ }
+
+ @Test
+ void putStoresValueAndType() {
+ byte[] value = "apple".getBytes(StandardCharsets.UTF_8);
+
+ cache.put("fruit", value, ValueType.STRING, 0);
+
+ Optional entry = cache.get("fruit");
+ assertTrue(entry.isPresent());
+ assertArrayEquals(value, entry.get().getValue());
+ assertEquals(ValueType.STRING, entry.get().getType());
+ }
+
+ @Test
+ void getReturnsStoredEntry() {
+ byte[] value = "apple".getBytes(StandardCharsets.UTF_8);
+
+ cache.put("fruit", value, ValueType.STRING, 0);
+
+ Optional entry = cache.get("fruit");
+ assertTrue(entry.isPresent());
+ assertArrayEquals(value, entry.get().getValue());
+ assertEquals(ValueType.STRING, entry.get().getType());
+ }
+
+ @Test
+ void getReturnsEmptyWhenKeyDoesNotExist() {
+ Optional entry = cache.get("missing");
+
+ assertFalse(entry.isPresent());
+ }
+
+ @Test
+ void deleteEntryAndReturnsEmpty() {
+ byte[] value = "apple".getBytes(StandardCharsets.UTF_8);
+
+ cache.put("fruit", value, ValueType.STRING, 0);
+
+ Optional entry = cache.get("fruit");
+ assertTrue(entry.isPresent());
+ assertArrayEquals(value, entry.get().getValue());
+ assertEquals(ValueType.STRING, entry.get().getType());
+
+ cache.delete("fruit");
+
+ entry = cache.get("fruit");
+ assertFalse(entry.isPresent());
+ }
+
+ @Test
+ void sizeReturnsNumberOfEntry() {
+ int givenSize = 3;
+ byte[] value = "apple".getBytes(StandardCharsets.UTF_8);
+
+ for (int i = 0; i < givenSize; i++) {
+ cache.put("fruit" + i, value, ValueType.STRING, 0);
+ }
+
+ int cacheSize = cache.size();
+
+ assertEquals(givenSize, cacheSize);
+ }
+
+ @Test
+ void clearReturnsEmptyCache() {
+ int givenSize = 3;
+ byte[] value = "apple".getBytes(StandardCharsets.UTF_8);
+
+ for (int i = 0; i < givenSize; i++) {
+ cache.put("fruit" + i, value, ValueType.STRING, 0);
+ }
+
+ int cacheSize = cache.size();
+
+ assertEquals(givenSize, cacheSize);
+
+ cache.clear();
+
+ int clearedSize = cache.size();
+ assertEquals(0, clearedSize);
+ }
+
+ @Test
+ void metricsReturnsSnapshot() {
+ cache.close();
+ cache = createCache(1);
+ byte[] value = "apple".getBytes(StandardCharsets.UTF_8);
+
+ cache.put("fruit", value, ValueType.STRING, 0);
+ cache.get("fruit");
+ cache.get("missing");
+
+ cache.put("color", value, ValueType.STRING, 0);
+
+ Snapshot metrics = cache.metrics();
+
+ assertEquals(1, metrics.getHits());
+ assertEquals(1, metrics.getMisses());
+ assertEquals(1, metrics.getEvictions());
+ assertEquals(0, metrics.getExpirations());
+ assertEquals(0.5, metrics.getHitRate());
+ }
+
+ private LocalCache createCache(int capacity) {
+ return new LocalCache<>(capacity, new LruEvictionPolicy<>());
+ }
+}
diff --git a/src/test/java/org/cache/eviction/LruEvictionPolicyTest.java b/src/test/java/org/cache/eviction/LruEvictionPolicyTest.java
new file mode 100644
index 0000000..5601054
--- /dev/null
+++ b/src/test/java/org/cache/eviction/LruEvictionPolicyTest.java
@@ -0,0 +1,44 @@
+package org.cache.eviction;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class LruEvictionPolicyTest {
+
+ private LruEvictionPolicy evictionPolicy;
+
+ @BeforeEach
+ void setUp() {
+ evictionPolicy = new LruEvictionPolicy<>();
+ }
+
+ @Test
+ void onKeyAddedMakesOldestKeyTheVictim() {
+ evictionPolicy.onKeyAdded("first");
+ evictionPolicy.onKeyAdded("second");
+
+ assertEquals("first", evictionPolicy.selectVictim().orElseThrow());
+ }
+
+ @Test
+ void onKeyAccessedMakesNotAccessedKeyTheVictim() {
+ evictionPolicy.onKeyAdded("first");
+ evictionPolicy.onKeyAdded("second");
+
+ evictionPolicy.onKeyAccessed("first");
+
+ assertEquals("second", evictionPolicy.selectVictim().orElseThrow());
+ }
+
+ @Test
+ void onKeyRemovedRemovesKeyFromVictimSelection() {
+ evictionPolicy.onKeyAdded("first");
+ evictionPolicy.onKeyAdded("second");
+
+ evictionPolicy.onKeyRemoved("first");
+
+ assertEquals("second", evictionPolicy.selectVictim().orElseThrow());
+ }
+}
diff --git a/src/test/java/org/cache/eviction/MruEvictionPolicyTest.java b/src/test/java/org/cache/eviction/MruEvictionPolicyTest.java
new file mode 100644
index 0000000..a47e087
--- /dev/null
+++ b/src/test/java/org/cache/eviction/MruEvictionPolicyTest.java
@@ -0,0 +1,44 @@
+package org.cache.eviction;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MruEvictionPolicyTest {
+
+ private MruEvictionPolicy evictionPolicy;
+
+ @BeforeEach
+ void setUp() {
+ evictionPolicy = new MruEvictionPolicy<>();
+ }
+
+ @Test
+ void onKeyAddedMakesNewestKeyTheVictim() {
+ evictionPolicy.onKeyAdded("first");
+ evictionPolicy.onKeyAdded("second");
+
+ assertEquals("second", evictionPolicy.selectVictim().orElseThrow());
+ }
+
+ @Test
+ void onKeyAccessedMakesAccessedKeyTheVictim() {
+ evictionPolicy.onKeyAdded("first");
+ evictionPolicy.onKeyAdded("second");
+
+ evictionPolicy.onKeyAccessed("first");
+
+ assertEquals("first", evictionPolicy.selectVictim().orElseThrow());
+ }
+
+ @Test
+ void onKeyRemovedRemovesKeyFromVictimSelection() {
+ evictionPolicy.onKeyAdded("first");
+ evictionPolicy.onKeyAdded("second");
+
+ evictionPolicy.onKeyRemoved("second");
+
+ assertEquals("first", evictionPolicy.selectVictim().orElseThrow());
+ }
+}
diff --git a/src/test/java/org/cache/network/TcpCacheServerTest.java b/src/test/java/org/cache/network/TcpCacheServerTest.java
new file mode 100644
index 0000000..195d06b
--- /dev/null
+++ b/src/test/java/org/cache/network/TcpCacheServerTest.java
@@ -0,0 +1,34 @@
+package org.cache.network;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.concurrent.ExecutorService;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class TcpCacheServerTest {
+
+ @Test
+ void startSubmitsHandlerForAcceptedSocket() throws Exception {
+ ServerSocket serverSocket = mock(ServerSocket.class);
+ Socket acceptedSocket = mock(Socket.class);
+ ExecutorService executor = mock(ExecutorService.class);
+ Runnable handler = mock(Runnable.class);
+ ClientConnectionHandlerFactory handlerFactory = mock(ClientConnectionHandlerFactory.class);
+
+ when(serverSocket.getLocalPort()).thenReturn(2020);
+ when(serverSocket.isClosed()).thenReturn(false, true);
+ when(serverSocket.accept()).thenReturn(acceptedSocket);
+ when(handlerFactory.create(acceptedSocket)).thenReturn(handler);
+ var server = new TcpCacheServer(serverSocket, executor, handlerFactory);
+
+ server.start();
+
+ verify(handlerFactory).create(acceptedSocket);
+ verify(executor).submit(handler);
+ }
+}
diff --git a/src/test/java/org/cache/network/connection/AutoProtocolConnectionTest.java b/src/test/java/org/cache/network/connection/AutoProtocolConnectionTest.java
new file mode 100644
index 0000000..3576c99
--- /dev/null
+++ b/src/test/java/org/cache/network/connection/AutoProtocolConnectionTest.java
@@ -0,0 +1,64 @@
+package org.cache.network.connection;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.net.Socket;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class AutoProtocolConnectionTest {
+
+ @Test
+ void readsLineCommandAndWritesLineResponse() throws Exception {
+ var output = new ByteArrayOutputStream();
+ var connection = connection("GET fruit\r\n", output);
+
+ List command = connection.readCommand();
+ connection.write("VALUE apple");
+
+ assertEquals(List.of("GET", "fruit"), command);
+ assertEquals("VALUE apple\r\n", output.toString(UTF_8));
+ }
+
+ @Test
+ void readsRespCommandAndWritesRespResponse() throws Exception {
+ var output = new ByteArrayOutputStream();
+ var connection = connection("*2\r\n$3\r\nGET\r\n$5\r\nfruit\r\n", output);
+
+ List command = connection.readCommand();
+ connection.write("VALUE apple");
+
+ assertEquals(List.of("GET", "fruit"), command);
+ assertEquals("*2\r\n$5\r\nVALUE\r\n$5\r\napple\r\n", output.toString(UTF_8));
+ }
+
+ @Test
+ void switchesProtocolPerCommandOnSameConnection() throws Exception {
+ var output = new ByteArrayOutputStream();
+ var input = "PUSH fruits apple\r\n*2\r\n$3\r\nGET\r\n$6\r\nfruits\r\n";
+ var connection = connection(input, output);
+
+ List lineCommand = connection.readCommand();
+ connection.write("OK");
+ List respCommand = connection.readCommand();
+ connection.write("VALUE apple");
+
+ assertEquals(List.of("PUSH", "fruits", "apple"), lineCommand);
+ assertEquals(List.of("GET", "fruits"), respCommand);
+ assertEquals("OK\r\n*2\r\n$5\r\nVALUE\r\n$5\r\napple\r\n", output.toString(UTF_8));
+ }
+
+ private static AutoProtocolConnection connection(String inputValue, ByteArrayOutputStream output) throws Exception {
+ Socket socket = mock(Socket.class);
+ when(socket.getInputStream()).thenReturn(new ByteArrayInputStream(inputValue.getBytes(UTF_8)));
+ when(socket.getOutputStream()).thenReturn(output);
+
+ return new AutoProtocolConnection(socket);
+ }
+}
diff --git a/src/test/java/org/cache/network/connection/ClientConnectionHandlerTest.java b/src/test/java/org/cache/network/connection/ClientConnectionHandlerTest.java
new file mode 100644
index 0000000..2384e31
--- /dev/null
+++ b/src/test/java/org/cache/network/connection/ClientConnectionHandlerTest.java
@@ -0,0 +1,36 @@
+package org.cache.network.connection;
+
+import org.cache.protocol.CommandProcessor;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.net.Socket;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class ClientConnectionHandlerTest {
+
+ @Test
+ void runProcessesCommandAndWritesResponse() throws Exception {
+ Socket socket = mock(Socket.class);
+ CommandProcessor> commandProcessor = mock(CommandProcessor.class);
+ var input = new ByteArrayInputStream("GET fruit\r\n".getBytes(UTF_8));
+ var output = new ByteArrayOutputStream();
+
+ when(socket.getInputStream()).thenReturn(input);
+ when(socket.getOutputStream()).thenReturn(output);
+ when(commandProcessor.process(List.of("GET", "fruit"))).thenReturn("VALUE apple");
+
+ new ClientConnectionHandler(socket, commandProcessor).run();
+
+ verify(commandProcessor).process(List.of("GET", "fruit"));
+ verify(socket).close();
+ assertEquals("VALUE apple\r\n", output.toString(UTF_8));
+ }
+}
diff --git a/src/test/java/org/cache/network/connection/LineProtocolConnectionTest.java b/src/test/java/org/cache/network/connection/LineProtocolConnectionTest.java
new file mode 100644
index 0000000..6abccac
--- /dev/null
+++ b/src/test/java/org/cache/network/connection/LineProtocolConnectionTest.java
@@ -0,0 +1,58 @@
+package org.cache.network.connection;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.net.Socket;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class LineProtocolConnectionTest {
+
+ private LineProtocolConnection lineProtocolConnection;
+ private ByteArrayOutputStream output;
+
+ @BeforeEach
+ void setup() throws Exception {
+ Socket socket = mock(Socket.class);
+
+ var input = new ByteArrayInputStream("GET fruit\r\n".getBytes(UTF_8));
+ output = new ByteArrayOutputStream();
+
+ when(socket.getInputStream()).thenReturn(input);
+ when(socket.getOutputStream()).thenReturn(output);
+
+ lineProtocolConnection = new LineProtocolConnection(socket);
+ }
+
+ @Test
+ void readCommandSplitsLineIntoParts() throws Exception {
+ List command = lineProtocolConnection.readCommand();
+
+ assertEquals(List.of("GET", "fruit"), command);
+ }
+
+ @Test
+ void readLineReturnsLineWithoutCrlf() throws Exception {
+ String givenLine = lineProtocolConnection.readLine();
+
+ String expectedResponse = "GET fruit";
+
+ assertEquals(expectedResponse, givenLine);
+ }
+
+ @Test
+ void writeAppendsCrlf() throws Exception {
+ String givenValue = "VALUE 333";
+
+ lineProtocolConnection.write(givenValue);
+
+ assertEquals("VALUE 333\r\n", output.toString(UTF_8));
+ }
+}
diff --git a/src/test/java/org/cache/network/connection/RespConnectionTest.java b/src/test/java/org/cache/network/connection/RespConnectionTest.java
new file mode 100644
index 0000000..2c38bff
--- /dev/null
+++ b/src/test/java/org/cache/network/connection/RespConnectionTest.java
@@ -0,0 +1,71 @@
+package org.cache.network.connection;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.net.Socket;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RespConnectionTest {
+
+ @Test
+ void readCommandReadsRespArray() throws Exception {
+ var connection = connectionWithInput("*2\r\n$3\r\nGET\r\n$5\r\nfruit\r\n");
+
+ List command = connection.readCommand();
+
+ assertEquals(List.of("GET", "fruit"), command);
+ }
+
+ @Test
+ void writeValueResponseAsRespArray() throws Exception {
+ var output = new ByteArrayOutputStream();
+ var connection = connectionWithOutput(output);
+
+ connection.write("VALUE apple");
+
+ assertEquals("*2\r\n$5\r\nVALUE\r\n$5\r\napple\r\n", output.toString(UTF_8));
+ }
+
+ @Test
+ void writeListResponseAsRespArrayOfValues() throws Exception {
+ var output = new ByteArrayOutputStream();
+ var connection = connectionWithOutput(output);
+
+ connection.write("LIST apple, banana");
+
+ assertEquals("*2\r\n$5\r\napple\r\n$6\r\nbanana\r\n", output.toString(UTF_8));
+ }
+
+ @Test
+ void writeEmptyListResponseAsEmptyRespArray() throws Exception {
+ var output = new ByteArrayOutputStream();
+ var connection = connectionWithOutput(output);
+
+ connection.write("LIST");
+
+ assertEquals("*0\r\n", output.toString(UTF_8));
+ }
+
+ private static RespConnection connectionWithInput(String inputValue) throws Exception {
+ return connection(new ByteArrayInputStream(inputValue.getBytes(UTF_8)), new ByteArrayOutputStream());
+ }
+
+ private static RespConnection connectionWithOutput(ByteArrayOutputStream output) throws Exception {
+ return connection(new ByteArrayInputStream(new byte[0]), output);
+ }
+
+ private static RespConnection connection(ByteArrayInputStream input, ByteArrayOutputStream output) throws Exception {
+ Socket socket = mock(Socket.class);
+ when(socket.getInputStream()).thenReturn(input);
+ when(socket.getOutputStream()).thenReturn(output);
+
+ return new RespConnection(socket);
+ }
+}
diff --git a/src/test/java/org/cache/protocol/CommandParserTest.java b/src/test/java/org/cache/protocol/CommandParserTest.java
new file mode 100644
index 0000000..1df45c7
--- /dev/null
+++ b/src/test/java/org/cache/protocol/CommandParserTest.java
@@ -0,0 +1,134 @@
+package org.cache.protocol;
+
+import org.cache.core.LocalCache;
+import org.cache.core.ValueType;
+import org.cache.eviction.LruEvictionPolicy;
+import org.cache.protocol.codec.ListValueCodec;
+import org.cache.protocol.codec.StringKeyCodec;
+import org.cache.protocol.codec.StringValueCodec;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.cache.protocol.commands.CacheCommand;
+import org.cache.protocol.commands.ClearCommand;
+import org.cache.protocol.commands.DeleteCommand;
+import org.cache.protocol.commands.GetCommand;
+import org.cache.protocol.commands.InvalidCommand;
+import org.cache.protocol.commands.LrangeCommand;
+import org.cache.protocol.commands.MetricsCommand;
+import org.cache.protocol.commands.PushCommand;
+import org.cache.protocol.commands.PutCommand;
+import org.cache.protocol.commands.SizeCommand;
+import org.cache.protocol.commands.UnknownCommand;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+class CommandParserTest {
+
+ private final CommandParser parser = new CommandParser<>(new StringKeyCodec());
+
+ @Test
+ void parseReturnsPutCommand() {
+ CacheCommand command = parser.parse(List.of("PUT", "fruit", "apple"));
+
+ assertInstanceOf(PutCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsGetCommand() {
+ CacheCommand command = parser.parse(List.of("GET", "fruit"));
+
+ assertInstanceOf(GetCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsDeleteCommand() {
+ CacheCommand command = parser.parse(List.of("DELETE", "fruit"));
+
+ assertInstanceOf(DeleteCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsPushCommand() {
+ CacheCommand command = parser.parse(List.of("PUSH", "fruits", "apple"));
+
+ assertInstanceOf(PushCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsLrangeCommandWithFromAndTo() {
+ CacheCommand command = parser.parse(List.of("LRANGE", "fruits", "1", "3"));
+
+ assertInstanceOf(LrangeCommand.class, command);
+ }
+
+ @Test
+ void parseLrangeWithoutFromUsesZeroAsFromIndex() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ cache.put("fruits", new ListValueCodec().encode(List.of("apple", "banana", "orange")), ValueType.LIST, 0);
+ CacheCommand command = parser.parse(List.of("LRANGE", "fruits", "2"));
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals("LIST apple, banana", response);
+ }
+ }
+
+ @Test
+ void parseReturnsSizeCommand() {
+ CacheCommand command = parser.parse(List.of("SIZE"));
+
+ assertInstanceOf(SizeCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsClearCommand() {
+ CacheCommand command = parser.parse(List.of("CLEAR"));
+
+ assertInstanceOf(ClearCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsMetricsCommand() {
+ CacheCommand command = parser.parse(List.of("METRICS"));
+
+ assertInstanceOf(MetricsCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsUnknownCommandForUnknownType() {
+ CacheCommand command = parser.parse(List.of("NOPE"));
+
+ assertInstanceOf(UnknownCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsInvalidCommandForWrongArgumentCount() {
+ CacheCommand command = parser.parse(List.of("GET"));
+
+ assertInstanceOf(InvalidCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsInvalidCommandForInvalidPutTtl() {
+ CacheCommand command = parser.parse(List.of("PUT", "fruit", "apple", "soon"));
+
+ assertInstanceOf(InvalidCommand.class, command);
+ }
+
+ @Test
+ void parseReturnsInvalidCommandForInvalidLrangeIndex() {
+ CacheCommand command = parser.parse(List.of("LRANGE", "fruits", "start", "2"));
+
+ assertInstanceOf(InvalidCommand.class, command);
+ }
+
+ private static ValueCodecRegistry valueCodecs() {
+ return new ValueCodecRegistry()
+ .register(ValueType.STRING, new StringValueCodec())
+ .register(ValueType.LIST, new ListValueCodec());
+ }
+}
diff --git a/src/test/java/org/cache/protocol/CommandProcessorTest.java b/src/test/java/org/cache/protocol/CommandProcessorTest.java
new file mode 100644
index 0000000..2844ab2
--- /dev/null
+++ b/src/test/java/org/cache/protocol/CommandProcessorTest.java
@@ -0,0 +1,35 @@
+package org.cache.protocol;
+
+import org.cache.core.Cache;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.cache.protocol.commands.CacheCommand;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class CommandProcessorTest {
+
+ @Test
+ void processParsesCommandAndReturnsCommandResponse() {
+ Cache cache = mock(Cache.class);
+ CommandParser parser = mock(CommandParser.class);
+ ValueCodecRegistry valueCodecs = mock(ValueCodecRegistry.class);
+ CacheCommand command = mock(CacheCommand.class);
+ List commandParts = List.of("GET", "fruit");
+
+ when(parser.parse(commandParts)).thenReturn(command);
+ when(command.process(cache, valueCodecs)).thenReturn("VALUE apple");
+ var processor = new CommandProcessor<>(cache, parser, valueCodecs);
+
+ String response = processor.process(commandParts);
+
+ verify(parser).parse(commandParts);
+ verify(command).process(cache, valueCodecs);
+ assertEquals("VALUE apple", response);
+ }
+}
diff --git a/src/test/java/org/cache/protocol/codec/IntegerKeyCodecTest.java b/src/test/java/org/cache/protocol/codec/IntegerKeyCodecTest.java
new file mode 100644
index 0000000..a20e2ad
--- /dev/null
+++ b/src/test/java/org/cache/protocol/codec/IntegerKeyCodecTest.java
@@ -0,0 +1,34 @@
+package org.cache.protocol.codec;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class IntegerKeyCodecTest {
+
+ private final IntegerKeyCodec codec = new IntegerKeyCodec();
+
+ @Test
+ void encodeConvertsIntegerToString() {
+ assertEquals("42", codec.encode(42));
+ }
+
+ @Test
+ void decodeConvertsNumericStringToInteger() {
+ assertEquals(42, codec.decode("42"));
+ }
+
+ @Test
+ void encodeAndDecodeEmptyValuesReturnNull() {
+ assertNull(codec.encode(null));
+ assertNull(codec.decode(null));
+ assertNull(codec.decode(" "));
+ }
+
+ @Test
+ void decodeThrowsForNonNumericString() {
+ assertThrows(IllegalArgumentException.class, () -> codec.decode("fruit"));
+ }
+}
diff --git a/src/test/java/org/cache/protocol/codec/ListValueCodecTest.java b/src/test/java/org/cache/protocol/codec/ListValueCodecTest.java
new file mode 100644
index 0000000..42dc3e6
--- /dev/null
+++ b/src/test/java/org/cache/protocol/codec/ListValueCodecTest.java
@@ -0,0 +1,37 @@
+package org.cache.protocol.codec;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ListValueCodecTest {
+
+ private final ListValueCodec codec = new ListValueCodec();
+
+ @Test
+ void encodeAndDecodeRoundTripsList() {
+ List value = List.of("apple", "banana");
+
+ List decoded = codec.decode(codec.encode(value));
+
+ assertEquals(value, decoded);
+ }
+
+ @Test
+ void encodeAndDecodeRoundTripsEmptyList() {
+ List value = List.of();
+
+ List decoded = codec.decode(codec.encode(value));
+
+ assertEquals(value, decoded);
+ }
+
+ @Test
+ void toStringJoinsListValues() {
+ byte[] encoded = codec.encode(List.of("apple", "banana"));
+
+ assertEquals("apple, banana", codec.toString(encoded));
+ }
+}
diff --git a/src/test/java/org/cache/protocol/codec/StringKeyCodecTest.java b/src/test/java/org/cache/protocol/codec/StringKeyCodecTest.java
new file mode 100644
index 0000000..7cd7aeb
--- /dev/null
+++ b/src/test/java/org/cache/protocol/codec/StringKeyCodecTest.java
@@ -0,0 +1,27 @@
+package org.cache.protocol.codec;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class StringKeyCodecTest {
+
+ private final StringKeyCodec codec = new StringKeyCodec();
+
+ @Test
+ void encodeReturnsSameValue() {
+ assertEquals("fruit", codec.encode("fruit"));
+ }
+
+ @Test
+ void decodeReturnsSameValue() {
+ assertEquals("fruit", codec.decode("fruit"));
+ }
+
+ @Test
+ void encodeAndDecodeAllowNull() {
+ assertNull(codec.encode(null));
+ assertNull(codec.decode(null));
+ }
+}
diff --git a/src/test/java/org/cache/protocol/codec/StringValueCodecTest.java b/src/test/java/org/cache/protocol/codec/StringValueCodecTest.java
new file mode 100644
index 0000000..9be1e1a
--- /dev/null
+++ b/src/test/java/org/cache/protocol/codec/StringValueCodecTest.java
@@ -0,0 +1,34 @@
+package org.cache.protocol.codec;
+
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class StringValueCodecTest {
+
+ private final StringValueCodec codec = new StringValueCodec();
+
+ @Test
+ void encodeConvertsStringToUtf8Bytes() {
+ assertArrayEquals("apple".getBytes(UTF_8), codec.encode("apple"));
+ }
+
+ @Test
+ void decodeConvertsUtf8BytesToString() {
+ assertEquals("apple", codec.decode("apple".getBytes(UTF_8)));
+ }
+
+ @Test
+ void toStringConvertsUtf8BytesToString() {
+ assertEquals("apple", codec.toString("apple".getBytes(UTF_8)));
+ }
+
+ @Test
+ void encodeAndDecodeThrowForNull() {
+ assertThrows(CodecConversionException.class, () -> codec.encode(null));
+ assertThrows(CodecConversionException.class, () -> codec.decode(null));
+ }
+}
diff --git a/src/test/java/org/cache/protocol/codec/ValueCodecRegistryTest.java b/src/test/java/org/cache/protocol/codec/ValueCodecRegistryTest.java
new file mode 100644
index 0000000..6585eb8
--- /dev/null
+++ b/src/test/java/org/cache/protocol/codec/ValueCodecRegistryTest.java
@@ -0,0 +1,36 @@
+package org.cache.protocol.codec;
+
+import org.cache.core.ValueType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ValueCodecRegistryTest {
+
+ @Test
+ void getReturnsRegisteredCodec() {
+ var registry = new ValueCodecRegistry();
+ var codec = new StringValueCodec();
+
+ registry.register(ValueType.STRING, codec);
+
+ assertSame(codec, registry.get(ValueType.STRING));
+ }
+
+ @Test
+ void registerReturnsSameRegistryForChaining() {
+ var registry = new ValueCodecRegistry();
+
+ ValueCodecRegistry returned = registry.register(ValueType.STRING, new StringValueCodec());
+
+ assertSame(registry, returned);
+ }
+
+ @Test
+ void getThrowsForUnsupportedValueType() {
+ var registry = new ValueCodecRegistry();
+
+ assertThrows(IllegalArgumentException.class, () -> registry.get(ValueType.STRING));
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/ClearCommandTest.java b/src/test/java/org/cache/protocol/commands/ClearCommandTest.java
new file mode 100644
index 0000000..8536781
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/ClearCommandTest.java
@@ -0,0 +1,35 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.LocalCache;
+import org.cache.core.ValueType;
+import org.cache.eviction.LruEvictionPolicy;
+import org.cache.protocol.codec.StringValueCodec;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ClearCommandTest {
+
+
+ @Test
+ void processClearsCacheAndReturnsOk() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ ValueCodecRegistry codecRegistry = new ValueCodecRegistry()
+ .register(ValueType.STRING, new StringValueCodec());
+
+ cache.put("x", "123".getBytes(StandardCharsets.UTF_8), ValueType.STRING, 10000);
+
+ assertEquals(1, cache.size());
+
+ var clearCommand = new ClearCommand();
+
+ String response = clearCommand.process(cache, codecRegistry);
+
+ assertEquals(ResponseConstants.OK.name(), response);
+ assertEquals(0, cache.size());
+ }
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/DeleteCommandTest.java b/src/test/java/org/cache/protocol/commands/DeleteCommandTest.java
new file mode 100644
index 0000000..3b78320
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/DeleteCommandTest.java
@@ -0,0 +1,23 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.Cache;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+class DeleteCommandTest {
+
+ @Test
+ void processDeletesKeyAndReturnsOk() {
+ Cache cache = mock(Cache.class);
+ var command = new DeleteCommand<>("fruit");
+
+ String response = command.process(cache, new ValueCodecRegistry());
+
+ verify(cache).delete("fruit");
+ assertEquals(ResponseConstants.OK.name(), response);
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/GetCommandTest.java b/src/test/java/org/cache/protocol/commands/GetCommandTest.java
new file mode 100644
index 0000000..3eb7e65
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/GetCommandTest.java
@@ -0,0 +1,59 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.LocalCache;
+import org.cache.core.ValueType;
+import org.cache.eviction.LruEvictionPolicy;
+import org.cache.protocol.codec.ListValueCodec;
+import org.cache.protocol.codec.StringValueCodec;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class GetCommandTest {
+
+ @Test
+ void processReturnsValueForStringEntry() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ cache.put("fruit", new StringValueCodec().encode("apple"), ValueType.STRING, 0);
+ var command = new GetCommand("fruit");
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals("VALUE apple", response);
+ }
+ }
+
+ @Test
+ void processReturnsNotFoundForMissingKey() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var command = new GetCommand("missing");
+
+ String response = command.process(cache, valueCodecs());
+
+ assertEquals(ResponseConstants.NOT_FOUND.name(), response);
+ }
+ }
+
+ @Test
+ void processReturnsErrorForListEntry() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ cache.put("fruit", new ListValueCodec().encode(List.of("apple")), ValueType.LIST, 0);
+ var command = new GetCommand("fruit");
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals("ERROR key contains list value", response);
+ }
+ }
+
+ private static ValueCodecRegistry valueCodecs() {
+ return new ValueCodecRegistry()
+ .register(ValueType.STRING, new StringValueCodec())
+ .register(ValueType.LIST, new ListValueCodec());
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/InvalidCommandTest.java b/src/test/java/org/cache/protocol/commands/InvalidCommandTest.java
new file mode 100644
index 0000000..6a442aa
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/InvalidCommandTest.java
@@ -0,0 +1,20 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.Cache;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+
+class InvalidCommandTest {
+
+ @Test
+ void processReturnsErrorWithMessage() {
+ var command = new InvalidCommand("usage: GET key");
+
+ String response = command.process(mock(Cache.class), new ValueCodecRegistry());
+
+ assertEquals("ERROR usage: GET key", response);
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/LrangeCommandTest.java b/src/test/java/org/cache/protocol/commands/LrangeCommandTest.java
new file mode 100644
index 0000000..05acc47
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/LrangeCommandTest.java
@@ -0,0 +1,96 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.LocalCache;
+import org.cache.core.ValueType;
+import org.cache.eviction.LruEvictionPolicy;
+import org.cache.protocol.codec.ListValueCodec;
+import org.cache.protocol.codec.StringValueCodec;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class LrangeCommandTest {
+
+ @Test
+ void processReturnsListRange() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ cache.put("fruits", new ListValueCodec().encode(List.of("apple", "banana", "orange")), ValueType.LIST, 0);
+ var command = new LrangeCommand("fruits", 0, 2);
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals("LIST apple, banana", response);
+ }
+ }
+
+ @Test
+ void processBoundsToIndexToListSize() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ cache.put("fruits", new ListValueCodec().encode(List.of("apple", "banana")), ValueType.LIST, 0);
+ var command = new LrangeCommand("fruits", 0, 10);
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals("LIST apple, banana", response);
+ }
+ }
+
+ @Test
+ void processReturnsEmptyListWhenFromIsOutOfRange() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ cache.put("fruits", new ListValueCodec().encode(List.of("apple")), ValueType.LIST, 0);
+ var command = new LrangeCommand("fruits", 2, 3);
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals(ResponseConstants.LIST.name(), response);
+ }
+ }
+
+ @Test
+ void processReturnsNotFoundForMissingKey() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var command = new LrangeCommand("missing", 0, 1);
+
+ String response = command.process(cache, valueCodecs());
+
+ assertEquals(ResponseConstants.NOT_FOUND.name(), response);
+ }
+ }
+
+ @Test
+ void processReturnsErrorForInvalidRange() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var command = new LrangeCommand("fruits", 2, 1);
+
+ String response = command.process(cache, valueCodecs());
+
+ assertEquals("ERROR invalid range: from must be >= 0 and to must be >= from", response);
+ }
+ }
+
+ @Test
+ void processReturnsErrorWhenKeyContainsString() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ cache.put("fruits", new StringValueCodec().encode("apple"), ValueType.STRING, 0);
+ var command = new LrangeCommand("fruits", 0, 1);
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals("ERROR key contains string value", response);
+ }
+ }
+
+ private static ValueCodecRegistry valueCodecs() {
+ return new ValueCodecRegistry()
+ .register(ValueType.STRING, new StringValueCodec())
+ .register(ValueType.LIST, new ListValueCodec());
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/MetricsCommandTest.java b/src/test/java/org/cache/protocol/commands/MetricsCommandTest.java
new file mode 100644
index 0000000..f5e240d
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/MetricsCommandTest.java
@@ -0,0 +1,24 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.Cache;
+import org.cache.core.metrics.Snapshot;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MetricsCommandTest {
+
+ @Test
+ void processReturnsMetricsSnapshotValues() {
+ Cache cache = mock(Cache.class);
+ when(cache.metrics()).thenReturn(new Snapshot(1, 2, 3, 4, 0.5));
+ var command = new MetricsCommand();
+
+ String response = command.process(cache, new ValueCodecRegistry());
+
+ assertEquals("METRICS hits=1 misses=2 evictions=3 expirations=4 hitRate=0.5", response);
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/PushCommandTest.java b/src/test/java/org/cache/protocol/commands/PushCommandTest.java
new file mode 100644
index 0000000..6e11fa7
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/PushCommandTest.java
@@ -0,0 +1,59 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.LocalCache;
+import org.cache.core.ValueType;
+import org.cache.eviction.LruEvictionPolicy;
+import org.cache.protocol.codec.ListValueCodec;
+import org.cache.protocol.codec.StringValueCodec;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class PushCommandTest {
+
+ @Test
+ void processCreatesListAndReturnsOk() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var command = new PushCommand<>("fruits", "apple", ValueType.LIST);
+
+ String response = command.process(cache, valueCodecs());
+
+ assertEquals(ResponseConstants.OK.name(), response);
+ assertEquals("LIST apple", new LrangeCommand("fruits", 0, 1).process(cache, valueCodecs()));
+ }
+ }
+
+ @Test
+ void processAppendsToExistingList() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ new PushCommand("fruits", "apple", ValueType.LIST).process(cache, valueCodecs);
+ var command = new PushCommand("fruits", "banana", ValueType.LIST);
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals(ResponseConstants.OK.name(), response);
+ assertEquals("LIST apple, banana", new LrangeCommand("fruits", 0, 2).process(cache, valueCodecs));
+ }
+ }
+
+ @Test
+ void processReturnsErrorWhenKeyContainsString() {
+ try (var cache = new LocalCache(10, new LruEvictionPolicy<>())) {
+ var valueCodecs = valueCodecs();
+ cache.put("fruits", new StringValueCodec().encode("apple"), ValueType.STRING, 0);
+ var command = new PushCommand("fruits", "banana", ValueType.LIST);
+
+ String response = command.process(cache, valueCodecs);
+
+ assertEquals("ERROR key contains string value", response);
+ }
+ }
+
+ private static ValueCodecRegistry valueCodecs() {
+ return new ValueCodecRegistry()
+ .register(ValueType.STRING, new StringValueCodec())
+ .register(ValueType.LIST, new ListValueCodec());
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/PutCommandTest.java b/src/test/java/org/cache/protocol/commands/PutCommandTest.java
new file mode 100644
index 0000000..72d7044
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/PutCommandTest.java
@@ -0,0 +1,33 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.Cache;
+import org.cache.core.ValueType;
+import org.cache.protocol.codec.StringValueCodec;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+class PutCommandTest {
+
+ @Test
+ void processStoresEncodedStringValueAndReturnsOk() {
+ Cache cache = mock(Cache.class);
+ var valueCodecs = new ValueCodecRegistry()
+ .register(ValueType.STRING, new StringValueCodec());
+ var command = new PutCommand<>("fruit", "apple", ValueType.STRING, 1_000);
+ ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(byte[].class);
+
+ String response = command.process(cache, valueCodecs);
+
+ verify(cache).put(eq("fruit"), valueCaptor.capture(), eq(ValueType.STRING), eq(1_000L));
+ assertArrayEquals("apple".getBytes(UTF_8), valueCaptor.getValue());
+ assertEquals(ResponseConstants.OK.name(), response);
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/SizeCommandTest.java b/src/test/java/org/cache/protocol/commands/SizeCommandTest.java
new file mode 100644
index 0000000..d95f407
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/SizeCommandTest.java
@@ -0,0 +1,23 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.Cache;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class SizeCommandTest {
+
+ @Test
+ void processReturnsCacheSize() {
+ Cache cache = mock(Cache.class);
+ when(cache.size()).thenReturn(3);
+ var command = new SizeCommand();
+
+ String response = command.process(cache, new ValueCodecRegistry());
+
+ assertEquals("SIZE 3", response);
+ }
+}
diff --git a/src/test/java/org/cache/protocol/commands/UnknownCommandTest.java b/src/test/java/org/cache/protocol/commands/UnknownCommandTest.java
new file mode 100644
index 0000000..448841c
--- /dev/null
+++ b/src/test/java/org/cache/protocol/commands/UnknownCommandTest.java
@@ -0,0 +1,20 @@
+package org.cache.protocol.commands;
+
+import org.cache.core.Cache;
+import org.cache.protocol.codec.ValueCodecRegistry;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+
+class UnknownCommandTest {
+
+ @Test
+ void processReturnsUnknownCommandError() {
+ var command = new UnknownCommand();
+
+ String response = command.process(mock(Cache.class), new ValueCodecRegistry());
+
+ assertEquals("ERROR unknown command", response);
+ }
+}