emqtt's transitive `quicer` dep needs CMake + msquic submodule clones to build, which the slim Debian builder image doesn't ship — the prod CI build was failing inside `mix deps.compile`. Tortoise311 is the obvious pure-Elixir alternative but it bundles a gen_state_machine + retry/inflight stack we don't use because we're subscribe-only at QoS 0. Implementing the wire protocol inline is ~150 lines in `Microwaveprop.Pskr.Mqtt` (CONNECT/SUBSCRIBE/PINGREQ/DISCONNECT builders, CONNACK/PUBLISH/SUBACK/PINGRESP parser, varint codec). Pskr.Client now uses :gen_tcp with `active: :once` framing, buffers partial reads, schedules its own keepalive, and parses inbound packets in a tight loop. Drops `BUILD_WITHOUT_QUIC=1` from the Dockerfile — no longer relevant since there's no native dep at all in the build chain now.
103 lines
3.4 KiB
Elixir
103 lines
3.4 KiB
Elixir
defmodule Microwaveprop.Pskr.MqttTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Pskr.Mqtt
|
|
|
|
describe "encode_varint/1 + decode_varint/1" do
|
|
# Boundary values from the MQTT 3.1.1 spec, table 2.4.
|
|
@cases [
|
|
{0, 1},
|
|
{127, 1},
|
|
{128, 2},
|
|
{16_383, 2},
|
|
{16_384, 3},
|
|
{2_097_151, 3},
|
|
{2_097_152, 4},
|
|
{268_435_455, 4}
|
|
]
|
|
|
|
test "round-trip encodes to the expected number of bytes" do
|
|
for {n, len} <- @cases do
|
|
encoded = Mqtt.encode_varint(n)
|
|
assert byte_size(encoded) == len, "len mismatch for #{n}"
|
|
assert {:ok, ^n, <<>>} = Mqtt.decode_varint(encoded)
|
|
end
|
|
end
|
|
|
|
test "decode returns :incomplete on truncated input" do
|
|
# 0x80 has the continuation bit set but no following byte.
|
|
assert :incomplete == Mqtt.decode_varint(<<0x80>>)
|
|
end
|
|
|
|
test "decode returns the unconsumed tail" do
|
|
assert {:ok, 200, "tail"} = Mqtt.decode_varint(Mqtt.encode_varint(200) <> "tail")
|
|
end
|
|
end
|
|
|
|
describe "connect/2" do
|
|
test "produces a CONNECT packet for the given client id and keepalive" do
|
|
packet = Mqtt.connect("test-client", 60)
|
|
|
|
# Fixed header: type=1, flags=0 → 0x10
|
|
assert <<0x10, _rest::binary>> = packet
|
|
|
|
# Body should contain the protocol header verbatim.
|
|
assert :binary.match(packet, "MQTT") != :nomatch
|
|
assert :binary.match(packet, "test-client") != :nomatch
|
|
end
|
|
end
|
|
|
|
describe "subscribe/2" do
|
|
test "encodes packet id and each topic with its QoS" do
|
|
packet = Mqtt.subscribe(42, [{"foo/bar", 0}, {"baz", 1}])
|
|
|
|
# Type=8, reserved bits=0010 → 0x82
|
|
assert <<0x82, _rest::binary>> = packet
|
|
assert :binary.match(packet, "foo/bar") != :nomatch
|
|
assert :binary.match(packet, "baz") != :nomatch
|
|
# Packet ID 42 lands in the variable header right after the
|
|
# remaining-length byte (which is < 128 → single byte).
|
|
assert <<0x82, _len, 0::8, 42::8, _::binary>> = packet
|
|
end
|
|
end
|
|
|
|
describe "parse/1" do
|
|
test "decodes CONNACK with session_present + return_code" do
|
|
assert {:ok, {:connack, 0, false}, ""} = Mqtt.parse(<<0x20, 0x02, 0x00, 0x00>>)
|
|
assert {:ok, {:connack, 5, true}, ""} = Mqtt.parse(<<0x20, 0x02, 0x01, 0x05>>)
|
|
end
|
|
|
|
test "decodes a QoS 0 PUBLISH" do
|
|
topic = "pskr/filter/v2/2m/FT8"
|
|
payload = ~s({"sq":1})
|
|
body = <<byte_size(topic)::16, topic::binary, payload::binary>>
|
|
packet = <<0x30, byte_size(body)>> <> body
|
|
|
|
assert {:ok, {:publish, ^topic, ^payload}, ""} = Mqtt.parse(packet)
|
|
end
|
|
|
|
test "decodes SUBACK with per-topic QoS results" do
|
|
assert {:ok, {:suback, 7, [0, 1, 0x80]}, ""} = Mqtt.parse(<<0x90, 0x05, 0::8, 7::8, 0, 1, 0x80>>)
|
|
end
|
|
|
|
test "decodes PINGRESP" do
|
|
assert {:ok, :pingresp, ""} = Mqtt.parse(<<0xD0, 0x00>>)
|
|
end
|
|
|
|
test "returns :incomplete on a short remaining-length read" do
|
|
# Just the type byte, no length yet.
|
|
assert {:incomplete, <<0x30>>} = Mqtt.parse(<<0x30>>)
|
|
end
|
|
|
|
test "returns :incomplete when body is shorter than declared length" do
|
|
# Says length=10 but only gives 3 bytes.
|
|
assert {:incomplete, _} = Mqtt.parse(<<0x30, 10, "abc">>)
|
|
end
|
|
|
|
test "returns trailing bytes after a complete packet" do
|
|
first = <<0xD0, 0x00>>
|
|
tail = <<0xC0, 0x00>>
|
|
assert {:ok, :pingresp, ^tail} = Mqtt.parse(first <> tail)
|
|
end
|
|
end
|
|
end
|