Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.0

require (
github.com/Azure/go-amqp v1.5.1
github.com/docker/docker v28.5.2+incompatible
github.com/google/uuid v1.6.0
github.com/spf13/cobra v1.10.2
github.com/testcontainers/testcontainers-go v0.41.0
Expand All @@ -23,7 +24,6 @@ require (
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
Expand Down
20 changes: 12 additions & 8 deletions internal/broker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type Client struct {
// bounded by ctx. On any failure it cleans up a half-open connection and
// returns a wrapped error. The returned Client must be closed with Close.
func Connect(ctx context.Context, p ConnectionProps) (*Client, error) {
conn, err := amqp.Dial(ctx, fmt.Sprintf("amqp://%s", buildConnectionURL(p)), nil)
conn, err := amqp.Dial(ctx, fmt.Sprintf("amqp://%s", p.URL), connOptions(p))
if err != nil {
return nil, fmt.Errorf("dial broker: %w", err)
}
Expand All @@ -50,6 +50,17 @@ func Connect(ctx context.Context, p ConnectionProps) (*Client, error) {
return &Client{conn: conn, sess: sess}, nil
}

// connOptions builds the SASL PLAIN options for p, or nil for an anonymous
// connection. Credentials travel in the SASL handshake rather than the dial
// URL, so passwords may contain any characters (/, =, @, ...) without needing
// URL escaping.
func connOptions(p ConnectionProps) *amqp.ConnOptions {
if p.Username == "" && p.Password == "" {
return nil
}
return &amqp.ConnOptions{SASLType: amqp.SASLTypePlain(p.Username, p.Password)}
}

// Session exposes the Client's default AMQP session so callers in this package
// can open their own links on it.
func (c *Client) Session() *amqp.Session { return c.sess }
Expand All @@ -66,10 +77,3 @@ func (c *Client) Close(ctx context.Context) error {
}
return nil
}

func buildConnectionURL(p ConnectionProps) string {
if p.Username != "" && p.Password != "" {
return fmt.Sprintf("%s:%s@%s", p.Username, p.Password, p.URL)
}
return p.URL
}
20 changes: 11 additions & 9 deletions internal/broker/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ package broker

import "testing"

func TestBuildConnectionURL(t *testing.T) {
func TestConnOptions(t *testing.T) {
cases := []struct {
name string
in ConnectionProps
want string
name string
in ConnectionProps
wantSet bool
}{
{"with creds", ConnectionProps{URL: "h:61616", Username: "a", Password: "b"}, "a:b@h:61616"},
{"no creds", ConnectionProps{URL: "h:61616"}, "h:61616"},
{"user only", ConnectionProps{URL: "h:61616", Username: "a"}, "h:61616"},
{"with creds", ConnectionProps{URL: "h:61616", Username: "a", Password: "b"}, true},
{"special char password", ConnectionProps{URL: "h:61616", Username: "a", Password: "X/Y="}, true},
{"no creds", ConnectionProps{URL: "h:61616"}, false},
{"user only", ConnectionProps{URL: "h:61616", Username: "a"}, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := buildConnectionURL(c.in); got != c.want {
t.Fatalf("got %q want %q", got, c.want)
opts := connOptions(c.in)
if got := opts != nil && opts.SASLType != nil; got != c.wantSet {
t.Fatalf("SASL set = %v, want %v", got, c.wantSet)
}
})
}
Expand Down
Loading