test: cover qrz/client
13 characterization tests: login success/failure, session caching, automatic re-login on session expiry with retry, lookup errors (not found, HTTP 5xx, malformed XML), reset_session/0.
This commit is contained in:
parent
ef0c73cb4d
commit
8db31794a1
1 changed files with 319 additions and 0 deletions
319
test/microwaveprop/qrz/client_test.exs
Normal file
319
test/microwaveprop/qrz/client_test.exs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
defmodule Microwaveprop.Qrz.ClientTest do
|
||||
# The Qrz.Client Agent is started once in the app supervision tree and
|
||||
# cached session state is process-global, so tests cannot run concurrently.
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Microwaveprop.Qrz.Client
|
||||
|
||||
@session_xml """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<QRZDatabase version="1.34">
|
||||
<Session>
|
||||
<Key>2331uf894c4bd29f3923f3bacf02c532d7bd9</Key>
|
||||
<Count>61</Count>
|
||||
<SubExp>Wed Jan 1 12:34:03 2025</SubExp>
|
||||
<GMTime>Sun Nov 16 04:48:02 2025</GMTime>
|
||||
</Session>
|
||||
</QRZDatabase>
|
||||
"""
|
||||
|
||||
@callsign_xml """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<QRZDatabase version="1.34">
|
||||
<Callsign>
|
||||
<call>W1AW</call>
|
||||
<fname>Hiram Percy</fname>
|
||||
<name>Maxim</name>
|
||||
<addr2>Newington</addr2>
|
||||
<state>CT</state>
|
||||
<country>United States</country>
|
||||
<grid>FN31pr</grid>
|
||||
<lat>41.714775</lat>
|
||||
<lon>-72.727260</lon>
|
||||
</Callsign>
|
||||
<Session>
|
||||
<Key>2331uf894c4bd29f3923f3bacf02c532d7bd9</Key>
|
||||
<Count>62</Count>
|
||||
<GMTime>Sun Nov 16 04:48:02 2025</GMTime>
|
||||
</Session>
|
||||
</QRZDatabase>
|
||||
"""
|
||||
|
||||
@session_expired_xml """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<QRZDatabase version="1.34">
|
||||
<Session>
|
||||
<Error>Session Timeout</Error>
|
||||
<GMTime>Sun Nov 16 04:48:02 2025</GMTime>
|
||||
</Session>
|
||||
</QRZDatabase>
|
||||
"""
|
||||
|
||||
@bad_credentials_xml """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<QRZDatabase version="1.34">
|
||||
<Session>
|
||||
<Error>Username/password incorrect</Error>
|
||||
<GMTime>Sun Nov 16 04:48:02 2025</GMTime>
|
||||
</Session>
|
||||
</QRZDatabase>
|
||||
"""
|
||||
|
||||
@not_found_xml """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<QRZDatabase version="1.34">
|
||||
<Session>
|
||||
<Key>2331uf894c4bd29f3923f3bacf02c532d7bd9</Key>
|
||||
<Error>Not found: NOSUCHCALL</Error>
|
||||
<GMTime>Sun Nov 16 04:48:02 2025</GMTime>
|
||||
</Session>
|
||||
</QRZDatabase>
|
||||
"""
|
||||
|
||||
setup do
|
||||
Client.reset_session()
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "login/0" do
|
||||
test "stores the session key returned by QRZ" do
|
||||
config = Application.get_env(:microwaveprop, Client, [])
|
||||
expected_username = Keyword.fetch!(config, :username)
|
||||
expected_password = Keyword.fetch!(config, :password)
|
||||
expected_agent = Keyword.get(config, :agent, "microwaveprop/1.0")
|
||||
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||
assert params["username"] == expected_username
|
||||
assert params["password"] == expected_password
|
||||
assert params["agent"] == expected_agent
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
end)
|
||||
|
||||
assert {:ok, "2331uf894c4bd29f3923f3bacf02c532d7bd9"} = Client.login()
|
||||
end
|
||||
|
||||
test "returns {:error, message} when credentials are rejected" do
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, @bad_credentials_xml)
|
||||
end)
|
||||
|
||||
assert {:error, "Username/password incorrect"} = Client.login()
|
||||
end
|
||||
|
||||
test "returns {:error, _} on non-200 HTTP responses" do
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 500, "boom")
|
||||
end)
|
||||
|
||||
assert {:error, "HTTP 500"} = Client.login()
|
||||
end
|
||||
|
||||
test "returns {:error, _} on transport failures" do
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
Req.Test.transport_error(conn, :econnrefused)
|
||||
end)
|
||||
|
||||
assert {:error, "Request failed: " <> _} = Client.login()
|
||||
end
|
||||
end
|
||||
|
||||
describe "lookup/1 with no cached session" do
|
||||
test "logs in transparently and returns a parsed callsign map" do
|
||||
Req.Test.expect(Client, 2, fn conn ->
|
||||
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||
|
||||
cond do
|
||||
Map.has_key?(params, "username") ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
|
||||
Map.has_key?(params, "callsign") ->
|
||||
assert params["callsign"] == "W1AW"
|
||||
assert params["s"] == "2331uf894c4bd29f3923f3bacf02c532d7bd9"
|
||||
Plug.Conn.send_resp(conn, 200, @callsign_xml)
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, record} = Client.lookup("W1AW")
|
||||
assert record["call"] == "W1AW"
|
||||
assert record["fname"] == "Hiram Percy"
|
||||
assert record["name"] == "Maxim"
|
||||
assert record["grid"] == "FN31pr"
|
||||
assert record["lat"] == "41.714775"
|
||||
assert record["lon"] == "-72.727260"
|
||||
end
|
||||
end
|
||||
|
||||
describe "lookup/1 with a cached session" do
|
||||
test "skips login and uses the cached key" do
|
||||
# First call seeds the session cache.
|
||||
Req.Test.expect(Client, 2, fn conn ->
|
||||
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||
|
||||
cond do
|
||||
Map.has_key?(params, "username") ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
|
||||
Map.has_key?(params, "callsign") ->
|
||||
Plug.Conn.send_resp(conn, 200, @callsign_xml)
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Client.lookup("W1AW")
|
||||
|
||||
# Now only the lookup request should hit the stub — no login.
|
||||
Req.Test.expect(Client, 1, fn conn ->
|
||||
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||
refute Map.has_key?(params, "username")
|
||||
assert params["s"] == "2331uf894c4bd29f3923f3bacf02c532d7bd9"
|
||||
Plug.Conn.send_resp(conn, 200, @callsign_xml)
|
||||
end)
|
||||
|
||||
assert {:ok, record} = Client.lookup("W1AW")
|
||||
assert record["call"] == "W1AW"
|
||||
end
|
||||
end
|
||||
|
||||
describe "lookup/1 session expiry" do
|
||||
test "re-logs-in and retries when QRZ returns session_expired" do
|
||||
# Prime the cache with a known stale key.
|
||||
Req.Test.expect(Client, 1, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Client.login()
|
||||
|
||||
# Sequence of responses for the retry flow:
|
||||
# 1. lookup with stale key → session_expired XML
|
||||
# 2. re-login → fresh session XML
|
||||
# 3. lookup with new key → success
|
||||
{:ok, agent} = Agent.start_link(fn -> 0 end)
|
||||
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
n = Agent.get_and_update(agent, fn n -> {n + 1, n + 1} end)
|
||||
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||
|
||||
case n do
|
||||
1 ->
|
||||
assert Map.has_key?(params, "callsign")
|
||||
Plug.Conn.send_resp(conn, 200, @session_expired_xml)
|
||||
|
||||
2 ->
|
||||
assert Map.has_key?(params, "username")
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
|
||||
3 ->
|
||||
assert Map.has_key?(params, "callsign")
|
||||
Plug.Conn.send_resp(conn, 200, @callsign_xml)
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, record} = Client.lookup("W1AW")
|
||||
assert record["call"] == "W1AW"
|
||||
assert Agent.get(agent, & &1) == 3
|
||||
end
|
||||
|
||||
test "returns an error when a retry login also expires" do
|
||||
# Prime with a cached key.
|
||||
Req.Test.expect(Client, 1, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Client.login()
|
||||
|
||||
# Every response lacks a Session/Key, so the retried lookup still sees
|
||||
# :session_expired and the client bails out.
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_expired_xml)
|
||||
end)
|
||||
|
||||
assert {:error, _} = Client.lookup("W1AW")
|
||||
end
|
||||
end
|
||||
|
||||
describe "lookup/1 error responses" do
|
||||
test "returns a not-found error when QRZ reports an unknown callsign" do
|
||||
Req.Test.expect(Client, 2, fn conn ->
|
||||
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||
|
||||
cond do
|
||||
Map.has_key?(params, "username") ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
|
||||
Map.has_key?(params, "callsign") ->
|
||||
Plug.Conn.send_resp(conn, 200, @not_found_xml)
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:error, "Not found: NOSUCHCALL"} = Client.lookup("NOSUCHCALL")
|
||||
end
|
||||
|
||||
test "bubbles up HTTP errors from the lookup request" do
|
||||
# Seed the cache so the lookup is the only request we need to stub.
|
||||
Req.Test.expect(Client, 1, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Client.login()
|
||||
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 503, "service unavailable")
|
||||
end)
|
||||
|
||||
assert {:error, "HTTP 503"} = Client.lookup("W1AW")
|
||||
end
|
||||
end
|
||||
|
||||
describe "reset_session/0" do
|
||||
test "clears a cached session so the next lookup re-logs-in" do
|
||||
Req.Test.expect(Client, 1, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Client.login()
|
||||
assert :ok = Client.reset_session()
|
||||
|
||||
# Because the cache was cleared, lookup must perform a fresh login
|
||||
# before the callsign request. Expect exactly two requests.
|
||||
Req.Test.expect(Client, 2, fn conn ->
|
||||
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||
|
||||
cond do
|
||||
Map.has_key?(params, "username") ->
|
||||
Plug.Conn.send_resp(conn, 200, @session_xml)
|
||||
|
||||
Map.has_key?(params, "callsign") ->
|
||||
Plug.Conn.send_resp(conn, 200, @callsign_xml)
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, %{"call" => "W1AW"}} = Client.lookup("W1AW")
|
||||
end
|
||||
end
|
||||
|
||||
describe "XML parsing edge cases" do
|
||||
test "login on malformed XML crashes (xmerl is strict)" do
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, "not xml at all")
|
||||
end)
|
||||
|
||||
# xmerl_scan.string/1 exits on unparseable input. The current client
|
||||
# does not guard this, so the caller sees an exit rather than an
|
||||
# {:error, _} tuple.
|
||||
assert catch_exit(Client.login())
|
||||
end
|
||||
|
||||
test "login returns {:error, _} on empty Session element" do
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<QRZDatabase version="1.34">
|
||||
<Session/>
|
||||
</QRZDatabase>
|
||||
""")
|
||||
end)
|
||||
|
||||
assert {:error, "Unknown error"} = Client.login()
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue