package mqtt import ( "strings" "testing" ) func TestPlaintextToAPublicHostIsRefused(t *testing.T) { // The payloads carry customer visit records and the connection carries the // tenant's broker password. A tcp:// URL to a public host is not a config // choice, it is a mistake — and one that WORKS, which is exactly why it // has to fail here rather than be noticed after a year of traffic. for _, url := range []string{ "tcp://broker.example.com:1883", "mqtt://66.116.226.234:1883", "tcp://10.0.0.5:1883", "tcp://[2001:db8::1]:1883", } { if _, err := NewClient(ClientOptions{BrokerURL: url}); err == nil || !strings.Contains(err.Error(), "refusing plaintext") { t.Errorf("%s was not refused (err=%v)", url, err) } } } func TestPlaintextToLocalhostIsAllowed(t *testing.T) { // Local testing against a Mosquitto on the same box crosses no network. // Checked at the transport gate rather than through NewClient: dialling a // port nothing is listening on burns the full 20s connect timeout, and a // slow test is a test people start skipping. for _, url := range []string{"tcp://127.0.0.1:1883", "tcp://localhost:1883", "mqtt://[::1]:1883"} { if err := checkTransport(url); err != nil { t.Errorf("loopback %s was refused: %v", url, err) } } } func TestPlaintextEscapeHatchIsExplicit(t *testing.T) { // An override must exist for a lab, but it has to be a deliberate act, // not a config field someone leaves set. t.Setenv("BEHAVISION_ALLOW_PLAINTEXT_MQTT", "1") if err := checkTransport("tcp://broker.example.com:1883"); err != nil { t.Fatalf("escape hatch did not apply: %v", err) } } func TestTLSUrlsSkipTheTransportCheck(t *testing.T) { for _, url := range []string{"tls://b:8883", "ssl://b:8883", "wss://b:443"} { if err := checkTransport(url); err != nil { t.Errorf("%s rejected: %v", url, err) } } } func TestAnEmptyBrokerUrlIsAnError(t *testing.T) { if _, err := NewClient(ClientOptions{}); err == nil { t.Fatal("empty broker url accepted") } } func TestTLSConfigRejectsAnUnreadableCA(t *testing.T) { // Silently falling back to system roots when a pinned CA is missing would // quietly undo the pinning. if _, err := tlsConfig(ClientOptions{CAFile: "/nonexistent/ca.pem"}); err == nil { t.Fatal("missing CA file accepted") } } func TestTLSConfigRejectsAFileWithNoCertificates(t *testing.T) { f := t.TempDir() + "/not-a-cert.pem" if err := writeFile(f, "hello"); err != nil { t.Fatal(err) } if _, err := tlsConfig(ClientOptions{CAFile: f}); err == nil { t.Fatal("a file with no PEM certificates was accepted as a CA") } } func TestTLSFloorIsTLS12(t *testing.T) { cfg, err := tlsConfig(ClientOptions{}) if err != nil { t.Fatal(err) } if cfg.MinVersion < 0x0303 { t.Fatalf("MinVersion %#x allows TLS below 1.2", cfg.MinVersion) } } func TestPublishOnADeadClientErrorsRatherThanPanics(t *testing.T) { // The pump calls this on every tick; a nil-client panic would take the // whole agent down instead of backing off. c := &Client{} if err := c.Publish(nil, "t", []byte("{}")); err == nil { //nolint:staticcheck t.Fatal("publish on an unconnected client reported success") } if c.Connected() { t.Fatal("an unconnected client reported Connected") } } func writeFile(path, content string) error { return osWriteFile(path, []byte(content), 0o600) }