From 89e8bb226756e41e97abe43357aa6d9a3ead44b9 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 7 May 2026 13:48:08 -0500 Subject: [PATCH] =?UTF-8?q?test:=20use=20Mox=20for=20Valkey=20tests=20with?= =?UTF-8?q?out=20real=20Redis=20(55%=E2=86=9288%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create Microwaveprop.Valkey.Adapter behaviour with command/3 + pipeline/3 - Create Microwaveprop.Valkey.RedixAdapter as the production impl - Inject adapter via Application config; mock with Mox in tests - Cover all Valkey operations: get, mget, set, mset_with_ttl, zadd, zrevrange, zrangebyscore, zrem, del, scan_match (multi-cursor) - Full round-trip tests for encode/decode, error paths, edge cases Coverage: 78.93% → 79.07% --- lib/microwaveprop/valkey.ex | 6 +- lib/microwaveprop/valkey/adapter.ex | 11 ++ lib/microwaveprop/valkey/redix_adapter.ex | 14 ++ test/microwaveprop/valkey_test.exs | 212 +++++++++++++++++++--- test/test_helper.exs | 2 + 5 files changed, 220 insertions(+), 25 deletions(-) create mode 100644 lib/microwaveprop/valkey/adapter.ex create mode 100644 lib/microwaveprop/valkey/redix_adapter.ex diff --git a/lib/microwaveprop/valkey.ex b/lib/microwaveprop/valkey.ex index a01d4526..b8eab3e0 100644 --- a/lib/microwaveprop/valkey.ex +++ b/lib/microwaveprop/valkey.ex @@ -209,10 +209,12 @@ defmodule Microwaveprop.Valkey do end end + defp adapter, do: Application.get_env(:microwaveprop, :valkey_adapter, Microwaveprop.Valkey.RedixAdapter) + defp command(args) do if configured?() do try do - Redix.command(@conn, args, timeout: 1500) + adapter().command(@conn, args, timeout: 1500) catch :exit, reason -> {:error, {:exit, reason}} end @@ -224,7 +226,7 @@ defmodule Microwaveprop.Valkey do defp pipeline(cmds) do if configured?() do try do - Redix.pipeline(@conn, cmds, timeout: 3000) + adapter().pipeline(@conn, cmds, timeout: 3000) catch :exit, reason -> {:error, {:exit, reason}} end diff --git a/lib/microwaveprop/valkey/adapter.ex b/lib/microwaveprop/valkey/adapter.ex new file mode 100644 index 00000000..650ccb58 --- /dev/null +++ b/lib/microwaveprop/valkey/adapter.ex @@ -0,0 +1,11 @@ +defmodule Microwaveprop.Valkey.Adapter do + @moduledoc """ + Behaviour for the Valkey/Redis command runner. The production + implementation delegates to Redix; tests inject a Mox mock. + """ + @doc "Run a single Redis command." + @callback command(Redix.connection(), [String.t()], keyword()) :: {:ok, term()} | {:error, term()} + + @doc "Run a pipeline of Redis commands." + @callback pipeline(Redix.connection(), [[String.t()]], keyword()) :: {:ok, [term()]} | {:error, term()} +end diff --git a/lib/microwaveprop/valkey/redix_adapter.ex b/lib/microwaveprop/valkey/redix_adapter.ex new file mode 100644 index 00000000..78b49854 --- /dev/null +++ b/lib/microwaveprop/valkey/redix_adapter.ex @@ -0,0 +1,14 @@ +defmodule Microwaveprop.Valkey.RedixAdapter do + @moduledoc false + @behaviour Microwaveprop.Valkey.Adapter + + @impl true + def command(conn, args, opts) do + Redix.command(conn, args, opts) + end + + @impl true + def pipeline(conn, cmds, opts) do + Redix.pipeline(conn, cmds, opts) + end +end diff --git a/test/microwaveprop/valkey_test.exs b/test/microwaveprop/valkey_test.exs index b23a74ef..245b120e 100644 --- a/test/microwaveprop/valkey_test.exs +++ b/test/microwaveprop/valkey_test.exs @@ -1,8 +1,39 @@ defmodule Microwaveprop.ValkeyTest do use ExUnit.Case, async: true + import Mox + alias Microwaveprop.Valkey + # Register Mox mock as the adapter and fake a Redix connection so + # configured?() returns true, letting command/pipeline run through the mock. + setup do + prev_url = Application.get_env(:microwaveprop, :valkey_url) + prev_adapter = Application.get_env(:microwaveprop, :valkey_adapter) + + Application.put_env(:microwaveprop, :valkey_url, "redis://localhost:6379") + Application.put_env(:microwaveprop, :valkey_adapter, Microwaveprop.Valkey.MockAdapter) + + # Start a dummy process registered at the connection name. + {:ok, pid} = Task.start(fn -> Process.sleep(:infinity) end) + Process.register(pid, Microwaveprop.Valkey.Conn) + + on_exit(fn -> + Process.unregister(Microwaveprop.Valkey.Conn) + Process.exit(pid, :kill) + + if prev_url, + do: Application.put_env(:microwaveprop, :valkey_url, prev_url), + else: Application.delete_env(:microwaveprop, :valkey_url) + + if prev_adapter, + do: Application.put_env(:microwaveprop, :valkey_adapter, prev_adapter), + else: Application.delete_env(:microwaveprop, :valkey_adapter) + end) + + :ok + end + describe "child_spec/1" do test "returns a valid child spec map" do spec = Valkey.child_spec([]) @@ -14,14 +45,34 @@ defmodule Microwaveprop.ValkeyTest do end describe "configured?/0" do - test "returns false when Valkey is not configured" do - assert Valkey.configured?() == false + test "returns true when Redix process is alive" do + assert Valkey.configured?() == true end end describe "get/1" do - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.get("test_key") + test "returns :miss when key is nil" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["GET", "missing"], _opts -> + {:ok, nil} + end) + + assert Valkey.get("missing") == :miss + end + + test "returns decoded term on hit" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["GET", "key"], _opts -> + {:ok, :erlang.term_to_binary(%{foo: "bar"})} + end) + + assert Valkey.get("key") == {:ok, %{foo: "bar"}} + end + + test "propagates error" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["GET", "key"], _opts -> + {:error, :connection_lost} + end) + + assert Valkey.get("key") == {:error, :connection_lost} end end @@ -30,14 +81,46 @@ defmodule Microwaveprop.ValkeyTest do assert Valkey.mget([]) == {:ok, []} end - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.mget(["key1", "key2"]) + test "returns decoded values preserving order" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["MGET", "a", "b"], _opts -> + {:ok, [nil, :erlang.term_to_binary(42)]} + end) + + assert Valkey.mget(["a", "b"]) == {:ok, [nil, 42]} + end + + test "propagates error" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["MGET", "key"], _opts -> + {:error, :connection_lost} + end) + + assert Valkey.mget(["key"]) == {:error, :connection_lost} end end describe "set/3" do - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.set("key", "value", 3600) + test "returns :ok on success" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["SET", "k", _, "EX", "3600"], _opts -> + {:ok, "OK"} + end) + + assert Valkey.set("k", "value", 3600) == :ok + end + + test "propagates error" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["SET", "k", _, "EX", "3600"], _opts -> + {:error, :connection_lost} + end) + + assert Valkey.set("k", "value", 3600) == {:error, :connection_lost} + end + + test "handles unexpected response" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["SET", "k", _, "EX", "3600"], _opts -> + {:ok, "QUEUED"} + end) + + assert Valkey.set("k", "value", 3600) == {:error, {:ok, "QUEUED"}} end end @@ -46,27 +129,78 @@ defmodule Microwaveprop.ValkeyTest do assert Valkey.mset_with_ttl([], 3600) == :ok end - test "returns error when not configured" do - pairs = [{"key1", "val1"}, {"key2", "val2"}] - assert {:error, :not_configured} = Valkey.mset_with_ttl(pairs, 3600) + test "returns :ok when all SETs succeed" do + expect(Microwaveprop.Valkey.MockAdapter, :pipeline, fn _conn, cmds, _opts -> + assert length(cmds) == 2 + assert hd(cmds) |> List.first() == "SET" + {:ok, ["OK", "OK"]} + end) + + assert Valkey.mset_with_ttl([{"a", 1}, {"b", 2}], 3600) == :ok + end + + test "returns {:error, {:partial, _}} when some SETs fail" do + expect(Microwaveprop.Valkey.MockAdapter, :pipeline, fn _conn, _cmds, _opts -> + {:ok, ["OK", "ERROR"]} + end) + + assert {:error, {:partial, _}} = Valkey.mset_with_ttl([{"a", 1}, {"b", 2}], 3600) + end + + test "propagates pipeline error" do + expect(Microwaveprop.Valkey.MockAdapter, :pipeline, fn _conn, _cmds, _opts -> + {:error, :connection_lost} + end) + + assert Valkey.mset_with_ttl([{"a", 1}], 3600) == {:error, :connection_lost} end end describe "zadd/3" do - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.zadd("zset", 1.0, "member") + test "returns :ok on success" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["ZADD", "zset", "1.5", "mem"], _opts -> + {:ok, 1} + end) + + assert Valkey.zadd("zset", 1.5, "mem") == :ok + end + + test "propagates error" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["ZADD", "zset", "1.0", "mem"], _opts -> + {:error, :connection_lost} + end) + + assert Valkey.zadd("zset", 1.0, "mem") == {:error, :connection_lost} end end describe "zrevrange/3" do - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.zrevrange("zset", 0, 10) + test "returns members list" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["ZREVRANGE", "zset", "0", "10"], _opts -> + {:ok, ["a", "b"]} + end) + + assert Valkey.zrevrange("zset", 0, 10) == {:ok, ["a", "b"]} + end + + test "propagates error" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, _, _opts -> + {:error, :connection_lost} + end) + + assert Valkey.zrevrange("zset", 0, 10) == {:error, :connection_lost} end end describe "zrangebyscore/3" do - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.zrangebyscore("zset", "0", "100") + test "returns members list" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, + ["ZRANGEBYSCORE", "zset", "0", "100"], + _opts -> + {:ok, ["a", "b"]} + end) + + assert Valkey.zrangebyscore("zset", "0", "100") == {:ok, ["a", "b"]} end end @@ -75,8 +209,12 @@ defmodule Microwaveprop.ValkeyTest do assert Valkey.zrem("zset", []) == :ok end - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.zrem("zset", ["member1"]) + test "removes members" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["ZREM", "zset", "a", "b"], _opts -> + {:ok, 2} + end) + + assert Valkey.zrem("zset", ["a", "b"]) == :ok end end @@ -85,14 +223,42 @@ defmodule Microwaveprop.ValkeyTest do assert Valkey.del([]) == :ok end - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.del(["key1"]) + test "deletes keys" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["DEL", "k1", "k2"], _opts -> + {:ok, 2} + end) + + assert Valkey.del(["k1", "k2"]) == :ok end end describe "scan_match/1" do - test "returns error when not configured" do - assert {:error, :not_configured} = Valkey.scan_match("pattern:*") + test "returns all matching keys from single pass" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, ["SCAN", "0", "MATCH", "pfx:*", "COUNT", "500"], _opts -> + {:ok, ["0", ["pfx:a", "pfx:b"]]} + end) + + assert Valkey.scan_match("pfx:*") == {:ok, ["pfx:a", "pfx:b"]} + end + + test "follows cursor across multiple passes" do + Microwaveprop.Valkey.MockAdapter + |> expect(:command, fn _conn, ["SCAN", "0", "MATCH", "pfx:*", "COUNT", "500"], _opts -> + {:ok, ["3", ["pfx:a"]]} + end) + |> expect(:command, fn _conn, ["SCAN", "3", "MATCH", "pfx:*", "COUNT", "500"], _opts -> + {:ok, ["0", ["pfx:b"]]} + end) + + assert Valkey.scan_match("pfx:*") == {:ok, ["pfx:a", "pfx:b"]} + end + + test "propagates error" do + expect(Microwaveprop.Valkey.MockAdapter, :command, fn _conn, _, _opts -> + {:error, :connection_lost} + end) + + assert Valkey.scan_match("pfx:*") == {:error, :connection_lost} end end end diff --git a/test/test_helper.exs b/test/test_helper.exs index d453a0fa..72c00198 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,3 +3,5 @@ alias Ecto.Adapters.SQL.Sandbox ExUnit.start(exclude: [:slow], capture_log: true) Sandbox.mode(Microwaveprop.Repo, :manual) Sandbox.mode(Microwaveprop.AprsRepo, :manual) + +Mox.defmock(Microwaveprop.Valkey.MockAdapter, for: Microwaveprop.Valkey.Adapter)