prop/test/microwaveprop/valkey_test.exs
Graham McIntire 0c3be97abb
Some checks failed
Build base image / Build and push base image (push) Successful in 3m10s
Build and Push / Build and Push Docker Image (push) Failing after 14s
Build prop-grid-rs / Test, build, push (push) Successful in 12m52s
fix: resolve 27 security, architecture, test, and performance audit findings
P0 (security-critical):
- Gate CSV/ADIF upload tabs behind authentication, add 30s cooldown to all upload handlers
- Cap CSV/ADIF imports at 2,000 rows server-side in both parsers
- Add submitter_verified boolean to contacts (client-cannot-set, anonymous=false)
- Create k8s/secret.example.yaml with placeholders, add LIVE_VIEW_SIGNING_SALT

P1 (high-priority):
- Add Mox.verify_on_exit!() to valkey_test.exs
- Replace DateTime.utc_now() truncation with static ~U literals in map_live_test.exs
- Replace Process.sleep with render_async in pskr_spots_live_test.exs (6 occurrences)
- Add MonitorLive.Show test coverage (4 tests: owner view, non-owner redirect, config success/error)
- Extract duct-detection and mechanism-classification logic from ContactLive.Show into Propagation.PathAnalysis
- Split ContactLive.Show render into 12 function components
- Update CLAUDE.md: remove stale ML model, mark HRDPS active, add backtest/pskr dirs
- Batch CSV import enrichment jobs via new enqueue_for_contacts/1

P2 (medium-priority):
- Set secure:true on session and remember-me cookies in production
- Change SMTP TLS from verify_none to verify_peer with public_key cacerts
- Make /metrics fail-closed in production when PROMETHEUS_AUTH_TOKEN unset
- Add RateLimiter (anon_limit:10, auth_limit:60) to /api/contacts/map
- Add content-security-policy-report-only header
- Add comment noting String.to_atom is compile-time safe in hrdps_client.ex
- Delegate duplicated haversine_km to canonical Microwaveprop.Geo.haversine_km/4
- Consolidate score-tier/color/verdict formatting into Microwaveprop.Format
- Update CLAUDE.md testing section to match actual raw-string-matching practice
- Batch HrrrPointEnqueuer Repo.insert_all calls to single round-trip
- Split weather.ex (1696→216 lines) and radio.ex (1285→54 lines) into purpose-based sub-facades

P3 (low-priority):
- Add LIVE_VIEW_SIGNING_SALT warning comment, extend filter_parameters
- Add host/community validation to snmp_client.ex
- Add raw/1 safety comment in algo_live.ex
- Add hex-audit and cargo-audit Makefile targets
- Add privacy_live smoke test
- Replace notify_listener busy-poll loop with Process.monitor/1 + assert_receive
- Add ContactCommonVolumeRadar changeset validation tests (5 tests)
2026-07-27 18:19:37 -05:00

274 lines
7.9 KiB
Elixir

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
# Defined here (not in test_helper.exs) so test_helper can load even
# when the precommit chain has purged Mox from the code path.
alias Microwaveprop.Valkey
alias Microwaveprop.Valkey.Conn
alias Microwaveprop.Valkey.MockAdapter
Mox.defmock(MockAdapter, for: Microwaveprop.Valkey.Adapter)
# Register Mox mock as the adapter and fake a Redix connection so
# configured?() returns true, letting command/pipeline run through the mock.
setup do
Mox.verify_on_exit!()
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