mirror of
https://github.com/hanzoai/amqp.git
synced 2026-10-30 14:57:07 +00:00
The gateway answered a fixed table of four queue names and dropped, with a
warning, every message published to anything else. That is not AMQP support:
a standard client declaring its own queue watched its publishes vanish into a
broker that had said ok.
Translate the model instead of listing instances. A routing key and a NATS
subject are the same idea, so exchange + key becomes a subject, a queue
becomes a durable consumer, and a binding becomes that consumer's filter —
route.go is the whole map and it fits on a page. Topology lives in a KV bucket
every replica watches, so a queue declared through one pod is consumable
through the next and survives a restart.
What is implemented is now a whole path: declare, bind, publish with the full
property set, consume, ack, nack, reject, get, cancel, qos, publisher
confirms, the mandatory return, bodies over frame-max. What is not — headers
exchanges, transactions, byte-counted qos, '#' before the end of a binding key
— answers with the AMQP error the spec defines for it, on the channel, naming
the method. A client learns it asked for something absent.
Three defects the old path had, fixed by construction:
- It read the content header and body frames straight off the socket after
basic.publish, ignoring which channel they arrived on, so any interleaved
channel corrupted the message. Assembly now belongs to the channel.
- It trusted the 32-bit frame size, so seven octets from a stranger asked
for a 4 GiB allocation. wire.Reader refuses on the header.
- It auto-acked on the bus before the client saw the message and discarded
the client's basic.ack, so nothing was ever actually at-least-once.
Drops app.go and schema/: a broker's readiness is whether its port accepts,
which is what a TCP probe asks, and carrying a zip.App is what made the module
importable by exactly one host. The module now needs luxfi/log and nats.go and
nothing else.
Tests drive github.com/rabbitmq/amqp091-go unmodified against a real broker
over an embedded NATS — the only test that means anything for a wire protocol.
211 lines
6.3 KiB
Go
211 lines
6.3 KiB
Go
package wire
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// TestTableRoundTrip covers every field type the errata set defines. A table
|
|
// is the one structure a peer can put anything in, so a type this codec cannot
|
|
// spell is a message it cannot read.
|
|
func TestTableRoundTrip(t *testing.T) {
|
|
in := Table{
|
|
"bool": true,
|
|
"uint8": byte(200),
|
|
"int8": int8(-8),
|
|
"int16": int16(-1600),
|
|
"uint16": uint16(1600),
|
|
"int32": int32(-32000),
|
|
"uint32": uint32(32000),
|
|
"int64": int64(-64000000000),
|
|
"float32": float32(1.5),
|
|
"float64": float64(-2.25),
|
|
"decimal": Decimal{Scale: 2, Value: 1234},
|
|
"string": "a string with \x00 and \xff in it",
|
|
"bytes": []byte{0, 1, 2, 255},
|
|
"stamp": Stamp(1700000000),
|
|
"array": []any{int32(1), "two", true},
|
|
"table": Table{"nested": int32(9)},
|
|
"void": nil,
|
|
}
|
|
b := NewMethod(1, 2).Table(in)
|
|
got := NewArgs(b.Bytes()).Table()
|
|
if err := NewArgs(b.Bytes()).Err(); err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(in, got) {
|
|
t.Fatalf("table did not survive:\n in = %#v\nout = %#v", in, got)
|
|
}
|
|
}
|
|
|
|
// TestPropsRoundTrip: the content header and the bus carry the same octets, so
|
|
// one codec has to answer for both.
|
|
func TestPropsRoundTrip(t *testing.T) {
|
|
in := Props{
|
|
ContentType: "application/json",
|
|
ContentEncoding: "gzip",
|
|
Headers: Table{"n": int32(1)},
|
|
DeliveryMode: 2,
|
|
Priority: 5,
|
|
CorrelationID: "c",
|
|
ReplyTo: "r",
|
|
Expiration: "1000",
|
|
MessageID: "m",
|
|
Timestamp: 1700000000,
|
|
Type: "t",
|
|
UserID: "u",
|
|
AppID: "a",
|
|
}
|
|
got, err := DecodeProps(EncodeProps(in))
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(in, got) {
|
|
t.Fatalf("properties did not survive:\n in = %#v\nout = %#v", in, got)
|
|
}
|
|
|
|
// Absent is absent: nothing set means no presence bits and nothing after
|
|
// them, which is the two-octet header a body-only publish sends.
|
|
empty := EncodeProps(Props{})
|
|
if len(empty) != 2 || empty[0] != 0 || empty[1] != 0 {
|
|
t.Fatalf("empty properties encoded as %x, want 0000", empty)
|
|
}
|
|
}
|
|
|
|
// TestBitsPack: consecutive booleans share an octet, least significant first,
|
|
// and the next non-bit field starts a fresh one (§4.2.5.2). Getting this wrong
|
|
// shifts every field after it.
|
|
func TestBitsPack(t *testing.T) {
|
|
b := NewMethod(1, 2).Bit(true).Bit(false).Bit(true).ShortStr("x").Bit(true)
|
|
a := NewArgs(b.Bytes())
|
|
for i, want := range []bool{true, false, true} {
|
|
if got := a.Bit(); got != want {
|
|
t.Fatalf("bit %d = %v, want %v", i, got, want)
|
|
}
|
|
}
|
|
if s := a.ShortStr(); s != "x" {
|
|
t.Fatalf("string after bits = %q, want x", s)
|
|
}
|
|
if !a.Bit() {
|
|
t.Fatal("the bit after a string did not read back")
|
|
}
|
|
if err := a.Err(); err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
// Nine bits need two octets.
|
|
nine := &Buf{}
|
|
for range 9 {
|
|
nine.Bit(true)
|
|
}
|
|
if got := len(nine.Bytes()); got != 2 {
|
|
t.Fatalf("nine bits took %d octets, want 2", got)
|
|
}
|
|
}
|
|
|
|
// TestArgsStopsAtTheEnd: a truncated frame reads as an error, never as zeros a
|
|
// caller might mistake for a real value.
|
|
func TestArgsStopsAtTheEnd(t *testing.T) {
|
|
a := NewArgs([]byte{0, 10, 0, 20, 5}) // class, method, then a lone octet
|
|
a.LongLong()
|
|
if a.Err() == nil {
|
|
t.Fatal("reading 8 octets out of 1 reported no error")
|
|
}
|
|
if v := a.Short(); v != 0 {
|
|
t.Fatalf("a field read after a failure returned %d, want the zero value", v)
|
|
}
|
|
|
|
bad := NewArgs(append([]byte{0, 10, 0, 20}, 0, 0, 0, 4, 1, 'k', 'Z', 0))
|
|
bad.Table() // 'Z' is not a field type
|
|
if !errors.Is(bad.Err(), ErrSyntax) {
|
|
t.Fatalf("an unknown field type gave %v, want ErrSyntax", bad.Err())
|
|
}
|
|
}
|
|
|
|
// TestReaderRefusesAnOversizedFrame is the guard that keeps a stranger from
|
|
// asking for a 4 GiB allocation in seven octets.
|
|
func TestReaderRefusesAnOversizedFrame(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
buf.Write([]byte{Method, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF})
|
|
if _, err := NewReader(&buf, 4096).Read(); err == nil {
|
|
t.Fatal("a frame claiming 4 GiB was accepted")
|
|
}
|
|
|
|
// The refusal is on the header. Nothing was read past it, so a payload
|
|
// that large never had to exist.
|
|
var ok bytes.Buffer
|
|
ok.Write([]byte{Method, 0, 1, 0, 0, 0, 3})
|
|
ok.Write([]byte{1, 2, 3, End})
|
|
f, err := NewReader(&ok, 4096).Read()
|
|
if err != nil {
|
|
t.Fatalf("a frame within frame-max: %v", err)
|
|
}
|
|
if f.Channel != 1 || !bytes.Equal(f.Payload, []byte{1, 2, 3}) {
|
|
t.Fatalf("frame = %+v, want channel 1 payload 010203", f)
|
|
}
|
|
}
|
|
|
|
// TestReaderRefusesAMissingEndOctet: the end octet is the frame's own claim to
|
|
// be well formed, and a stream that has lost it cannot be resynchronised.
|
|
func TestReaderRefusesAMissingEndOctet(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
buf.Write([]byte{Method, 0, 0, 0, 0, 0, 1})
|
|
buf.Write([]byte{7, 0x00}) // payload, then the wrong end octet
|
|
if _, err := NewReader(&buf, 4096).Read(); err == nil {
|
|
t.Fatal("a frame with no end octet was accepted")
|
|
}
|
|
}
|
|
|
|
// TestContentSplitsToFitFrameMax: a body larger than one frame goes out as
|
|
// several, each within the limit, and the header still declares the whole.
|
|
func TestContentSplitsToFitFrameMax(t *testing.T) {
|
|
var sock bytes.Buffer
|
|
w := NewWriter(&sock, MinFrame)
|
|
body := bytes.Repeat([]byte{'x'}, MinFrame*3)
|
|
method := NewMethod(60, 60).ShortStr("tag").Bytes()
|
|
if err := w.Content(1, 60, method, Props{ContentType: "text/plain"}, body); err != nil {
|
|
t.Fatalf("content: %v", err)
|
|
}
|
|
|
|
r := NewReader(&sock, MinFrame)
|
|
f, err := r.Read()
|
|
if err != nil || f.Type != Method {
|
|
t.Fatalf("first frame = %+v, %v; want a method", f, err)
|
|
}
|
|
f, err = r.Read()
|
|
if err != nil || f.Type != Header {
|
|
t.Fatalf("second frame = %+v, %v; want a header", f, err)
|
|
}
|
|
class, size, props, err := ParseHeader(f.Payload)
|
|
if err != nil || class != 60 || size != uint64(len(body)) {
|
|
t.Fatalf("header: class %d size %d err %v", class, size, err)
|
|
}
|
|
if props.ContentType != "text/plain" {
|
|
t.Fatalf("content-type = %q", props.ContentType)
|
|
}
|
|
|
|
var got []byte
|
|
for {
|
|
f, err := r.Read()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("body frame: %v", err)
|
|
}
|
|
if f.Type != Body {
|
|
t.Fatalf("frame type %d after the header, want a body", f.Type)
|
|
}
|
|
if len(f.Payload) > MinFrame-8 {
|
|
t.Fatalf("body frame is %d octets, over the %d limit", len(f.Payload), MinFrame-8)
|
|
}
|
|
got = append(got, f.Payload...)
|
|
}
|
|
if !bytes.Equal(got, body) {
|
|
t.Fatalf("reassembled %d octets, sent %d", len(got), len(body))
|
|
}
|
|
}
|