diff --git a/test/excache_test.exs b/test/excache_test.exs new file mode 100644 index 0000000..a21498c --- /dev/null +++ b/test/excache_test.exs @@ -0,0 +1,124 @@ +defmodule ExcacheTest do + use Excache.Test.ServerCase, async: false + + test "PING and ECHO", %{conn: conn} do + assert {{:simple, "PONG"}, conn} = Client.command(conn, ["PING"]) + assert {"hi", conn} = Client.command(conn, ["PING", "hi"]) + assert {"hello world", _conn} = Client.command(conn, ["ECHO", "hello world"]) + end + + test "inline PING command over the socket", %{conn: conn} do + conn = Client.send_raw(conn, "PING\r\n") + assert {{:simple, "PONG"}, _conn} = Client.read(conn) + end + + test "SET, GET, TYPE and missing key", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "greeting", "hello"]) + assert {"hello", conn} = Client.command(conn, ["GET", "greeting"]) + assert {{:simple, "string"}, conn} = Client.command(conn, ["TYPE", "greeting"]) + assert {nil, conn} = Client.command(conn, ["GET", "absent"]) + assert {{:simple, "none"}, _conn} = Client.command(conn, ["TYPE", "absent"]) + end + + test "SET with NX and XX", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "lock", "a", "NX"]) + assert {nil, conn} = Client.command(conn, ["SET", "lock", "b", "NX"]) + assert {"a", conn} = Client.command(conn, ["GET", "lock"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "lock", "c", "XX"]) + assert {nil, _conn} = Client.command(conn, ["SET", "fresh", "z", "XX"]) + end + + test "INCR, DECR, INCRBY and non-integer error", %{conn: conn} do + assert {1, conn} = Client.command(conn, ["INCR", "counter"]) + assert {3, conn} = Client.command(conn, ["INCRBY", "counter", "2"]) + assert {2, conn} = Client.command(conn, ["DECR", "counter"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "word", "abc"]) + + assert {{:error, "ERR value is not an integer or out of range"}, _conn} = + Client.command(conn, ["INCR", "word"]) + end + + test "APPEND and GETSET", %{conn: conn} do + assert {5, conn} = Client.command(conn, ["APPEND", "buf", "hello"]) + assert {11, conn} = Client.command(conn, ["APPEND", "buf", " world"]) + assert {"hello world", conn} = Client.command(conn, ["GETSET", "buf", "fresh"]) + assert {"fresh", _conn} = Client.command(conn, ["GET", "buf"]) + end + + test "MSET and MGET", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MSET", "a", "1", "b", "2"]) + assert {["1", "2", nil], _conn} = Client.command(conn, ["MGET", "a", "b", "missing"]) + end + + test "DEL, EXISTS, KEYS, DBSIZE and FLUSHDB", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "user:1", "ann"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "user:2", "ben"]) + assert {2, conn} = Client.command(conn, ["EXISTS", "user:1", "user:2"]) + assert {2, conn} = Client.command(conn, ["DBSIZE"]) + + {keys, conn} = Client.command(conn, ["KEYS", "user:*"]) + assert Enum.sort(keys) == ["user:1", "user:2"] + + assert {1, conn} = Client.command(conn, ["DEL", "user:1"]) + assert {0, conn} = Client.command(conn, ["EXISTS", "user:1"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["FLUSHDB"]) + assert {0, _conn} = Client.command(conn, ["DBSIZE"]) + end + + test "EXPIRE, TTL, PTTL and PERSIST", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "ephemeral", "v", "EX", "100"]) + {ttl, conn} = Client.command(conn, ["TTL", "ephemeral"]) + assert ttl > 0 and ttl <= 100 + + assert {1, conn} = Client.command(conn, ["PERSIST", "ephemeral"]) + assert {-1, conn} = Client.command(conn, ["TTL", "ephemeral"]) + + assert {1, conn} = Client.command(conn, ["EXPIRE", "ephemeral", "50"]) + {pttl, conn} = Client.command(conn, ["PTTL", "ephemeral"]) + assert pttl > 40_000 and pttl <= 50_000 + + assert {-2, _conn} = Client.command(conn, ["TTL", "never-existed"]) + end + + test "PX expiry actually removes the key", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "blink", "v", "PX", "40"]) + Process.sleep(120) + assert {nil, _conn} = Client.command(conn, ["GET", "blink"]) + end + + test "WRONGTYPE guard across data types", %{conn: conn} do + assert {3, conn} = Client.command(conn, ["RPUSH", "aList", "a", "b", "c"]) + assert {{:error, "WRONGTYPE" <> _}, _conn} = Client.command(conn, ["GET", "aList"]) + end + + test "pub/sub delivers a published message", %{port: port, conn: subscriber} do + channel = "room:#{System.unique_integer([:positive])}" + + assert {["subscribe", ^channel, 1], subscriber} = + Client.command(subscriber, ["SUBSCRIBE", channel]) + + publisher = Client.connect(port) + assert {1, _publisher} = Client.command(publisher, ["PUBLISH", channel, "ping"]) + Client.close(publisher) + + assert {["message", ^channel, "ping"], _subscriber} = Client.read(subscriber) + end + + test "SAVE then restart reloads from disk", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "durable", "kept"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SAVE"]) + + Excache.Store.flushdb() + assert {nil, conn} = Client.command(conn, ["GET", "durable"]) + + Excache.Store.restart() + assert {"kept", _conn} = Client.command(conn, ["GET", "durable"]) + end + + test "partial frames are buffered until complete", %{conn: conn} do + conn = Client.send_raw(conn, "*3\r\n$3\r\nSET\r\n$4\r\npart") + conn = Client.send_raw(conn, "\r\n$2\r\nok\r\n") + assert {{:simple, "OK"}, conn} = Client.read(conn) + assert {"ok", _conn} = Client.command(conn, ["GET", "part"]) + end +end