From fbfda7b9f366f27b6905004f0367478559db553b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 18:07:00 +0200 Subject: [PATCH] test(transaction): add multi/exec transaction tests --- test/excache/transaction_test.exs | 111 ++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 test/excache/transaction_test.exs diff --git a/test/excache/transaction_test.exs b/test/excache/transaction_test.exs new file mode 100644 index 0000000..9d2b746 --- /dev/null +++ b/test/excache/transaction_test.exs @@ -0,0 +1,111 @@ +defmodule Excache.TransactionTest do + use Excache.Test.ServerCase, async: false + + test "MULTI queues commands and EXEC applies them in order", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["SET", "a", "1"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["INCR", "a"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["GET", "a"]) + + assert {[{:simple, "OK"}, 2, "2"], conn} = Client.command(conn, ["EXEC"]) + assert {"2", _conn} = Client.command(conn, ["GET", "a"]) + end + + test "DISCARD drops queued commands without applying them", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["SET", "gone", "v"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["DISCARD"]) + assert {nil, _conn} = Client.command(conn, ["GET", "gone"]) + end + + test "EXEC without a prior MULTI is an error", %{conn: conn} do + assert {{:error, "ERR EXEC without MULTI"}, _conn} = Client.command(conn, ["EXEC"]) + end + + test "DISCARD without a prior MULTI is an error", %{conn: conn} do + assert {{:error, "ERR DISCARD without MULTI"}, _conn} = Client.command(conn, ["DISCARD"]) + end + + test "nested MULTI is rejected", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:error, "ERR MULTI calls can not be nested"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "OK"}, _conn} = Client.command(conn, ["DISCARD"]) + end + + test "an unknown command queued inside MULTI aborts the whole transaction", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["SET", "kept", "no"]) + + assert {{:error, "ERR unknown command 'NOTACOMMAND'"}, conn} = + Client.command(conn, ["NOTACOMMAND"]) + + assert {{:error, "ERR EXECABORT" <> _}, conn} = Client.command(conn, ["EXEC"]) + assert {nil, _conn} = Client.command(conn, ["GET", "kept"]) + end + + test "WATCH aborts EXEC when the watched key changes before it runs", %{ + port: port, + conn: conn + } do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "balance", "100"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["WATCH", "balance"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["SET", "balance", "0"]) + + interloper = Client.connect(port) + assert {{:simple, "OK"}, _interloper} = Client.command(interloper, ["SET", "balance", "50"]) + Client.close(interloper) + + assert {nil, conn} = Client.command(conn, ["EXEC"]) + assert {"50", _conn} = Client.command(conn, ["GET", "balance"]) + end + + test "WATCH lets EXEC through when the watched key is untouched", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "balance", "100"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["WATCH", "balance"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["SET", "balance", "0"]) + assert {[{:simple, "OK"}], conn} = Client.command(conn, ["EXEC"]) + assert {"0", _conn} = Client.command(conn, ["GET", "balance"]) + end + + test "UNWATCH clears watches so a later EXEC is unaffected", %{port: port, conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "balance", "100"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["WATCH", "balance"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["UNWATCH"]) + + interloper = Client.connect(port) + assert {{:simple, "OK"}, _interloper} = Client.command(interloper, ["SET", "balance", "50"]) + Client.close(interloper) + + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["SET", "balance", "0"]) + assert {[{:simple, "OK"}], conn} = Client.command(conn, ["EXEC"]) + assert {"0", _conn} = Client.command(conn, ["GET", "balance"]) + end + + test "WATCH is rejected inside a MULTI block", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + + assert {{:error, "ERR WATCH inside MULTI is not allowed"}, conn} = + Client.command(conn, ["WATCH", "key"]) + + assert {[], _conn} = Client.command(conn, ["EXEC"]) + end + + test "EXEC clears watches even after a successful run", %{port: port, conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "balance", "100"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["WATCH", "balance"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["SET", "balance", "0"]) + assert {[{:simple, "OK"}], conn} = Client.command(conn, ["EXEC"]) + + interloper = Client.connect(port) + assert {{:simple, "OK"}, _interloper} = Client.command(interloper, ["SET", "balance", "77"]) + Client.close(interloper) + + assert {{:simple, "OK"}, conn} = Client.command(conn, ["MULTI"]) + assert {{:simple, "QUEUED"}, conn} = Client.command(conn, ["SET", "unrelated", "x"]) + assert {[{:simple, "OK"}], _conn} = Client.command(conn, ["EXEC"]) + end +end