defmodule Microwaveprop.ValkeyTest do # Setup registers a process under the global `Microwaveprop.Valkey.Conn` # name and swaps `:microwaveprop, :valkey_adapter` via Application.put_env. # Both are global state, so this case must not run concurrently with other # tests — otherwise ConnCase setups (which call GridCache.clear/0) see the # Mox adapter without expectations and crash unrelated tests. use ExUnit.Case, async: false 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. alias Microwaveprop.Valkey.Conn alias Microwaveprop.Valkey.MockAdapter 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, MockAdapter) # Start a dummy process registered at the connection name. {:ok, pid} = Task.start(fn -> Process.sleep(:infinity) end) Process.register(pid, Conn) on_exit(fn -> Process.unregister(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([]) assert spec.id == Valkey assert spec.type == :worker assert spec.restart == :permanent end end describe "configured?/0" do test "returns true when Redix process is alive" do assert Valkey.configured?() == true end end describe "get/1" do test "returns :miss when key is nil" do expect(MockAdapter, :command, fn _conn, ["GET", "missing"], _opts -> {:ok, nil} end) assert Valkey.get("missing") == :miss end test "returns decoded term on hit" do expect(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(MockAdapter, :command, fn _conn, ["GET", "key"], _opts -> {:error, :connection_lost} end) assert Valkey.get("key") == {:error, :connection_lost} end end describe "mget/1" do test "empty list returns {:ok, []}" do assert Valkey.mget([]) == {:ok, []} end test "returns decoded values preserving order" do expect(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(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 :ok on success" do expect(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(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(MockAdapter, :command, fn _conn, ["SET", "k", _, "EX", "3600"], _opts -> {:ok, "QUEUED"} end) assert Valkey.set("k", "value", 3600) == {:error, {:ok, "QUEUED"}} end end describe "mset_with_ttl/2" do test "empty list returns :ok" do assert Valkey.mset_with_ttl([], 3600) == :ok end test "returns :ok when all SETs succeed" do expect(MockAdapter, :pipeline, fn _conn, cmds, _opts -> assert length(cmds) == 2 assert cmds |> hd() |> 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(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(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 :ok on success" do expect(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(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 members list" do expect(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(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 members list" do expect(MockAdapter, :command, fn _conn, ["ZRANGEBYSCORE", "zset", "0", "100"], _opts -> {:ok, ["a", "b"]} end) assert Valkey.zrangebyscore("zset", "0", "100") == {:ok, ["a", "b"]} end end describe "zrem/2" do test "empty list returns :ok" do assert Valkey.zrem("zset", []) == :ok end test "removes members" do expect(MockAdapter, :command, fn _conn, ["ZREM", "zset", "a", "b"], _opts -> {:ok, 2} end) assert Valkey.zrem("zset", ["a", "b"]) == :ok end end describe "del/1" do test "empty list returns :ok" do assert Valkey.del([]) == :ok end test "deletes keys" do expect(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 all matching keys from single pass" do expect(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 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(MockAdapter, :command, fn _conn, _, _opts -> {:error, :connection_lost} end) assert Valkey.scan_match("pfx:*") == {:error, :connection_lost} end end end