package client_test import ( "crypto/tls" "crypto/x509" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/enmanuel/unibus/pkg/blobstore" "github.com/enmanuel/unibus/pkg/client" "github.com/enmanuel/unibus/pkg/embeddednats" "github.com/enmanuel/unibus/pkg/membership" "github.com/enmanuel/unibus/pkg/room" ) // TestConnectRequiresHTTPSWithCA covers audit H5's client contract: when a CA is // provided the control-plane URL must be https://. A signed request gives // integrity but not confidentiality, so silently talking http:// to a bus the // operator secured with a CA would leak all metadata to a MITM. Connect refuses // the plaintext URL outright (error path; the scheme is checked before any // network use, so a bogus CA path is irrelevant). func TestConnectRequiresHTTPSWithCA(t *testing.T) { _, err := client.Connect("nats://127.0.0.1:4222", "http://127.0.0.1:8470", mustIdentity(t), "/nonexistent/ca.crt") if err == nil { t.Fatalf("Connect with a CA and an http:// control plane must be refused") } if !strings.Contains(err.Error(), "https") { t.Fatalf("error should point the caller at https, got: %v", err) } } // TestControlPlaneOverTLS proves the control plane works over TLS pinned to the // bus CA (golden) and that a client lacking the CA cannot complete the handshake // (error path) — so a network observer can neither read nor inject control-plane // traffic. The data plane is left plaintext here to isolate the HTTP-TLS wiring. func TestControlPlaneOverTLS(t *testing.T) { dir := t.TempDir() store, err := membership.Open(filepath.Join(dir, "unibus.db")) if err != nil { t.Fatalf("store: %v", err) } t.Cleanup(func() { store.Close() }) blobs, err := blobstore.New(filepath.Join(dir, "blobs")) if err != nil { t.Fatalf("blobs: %v", err) } ns, err := embeddednats.StartServer(embeddednats.ServerConfig{ StoreDir: filepath.Join(dir, "js"), Host: "127.0.0.1", Port: freePort(t), }) if err != nil { t.Fatalf("nats: %v", err) } t.Cleanup(func() { ns.Shutdown(); ns.WaitForShutdown() }) natsURL := embeddednats.ClientURL(ns) // An https control plane wrapping the real membership server. ts := httptest.NewTLSServer(membership.NewServer(store, blobs, membership.AuthOff)) t.Cleanup(ts.Close) pool := x509.NewCertPool() pool.AddCert(ts.Certificate()) // Golden: trusting the control-plane CA, an https control-plane request works. good, err := client.NewWithOptions(natsURL, ts.URL, mustIdentity(t), client.Options{CtrlTLS: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}}) if err != nil { t.Fatalf("connect with the pinned CA: %v", err) } defer good.Close() if _, err := good.CreateRoom("room.tls.ctrl", room.ModeNATS); err != nil { t.Fatalf("control plane over TLS should succeed with the pinned CA: %v", err) } // Error path: without the CA the https handshake fails, so the request errors. bad, err := client.NewWithOptions(natsURL, ts.URL, mustIdentity(t), client.Options{CtrlTLS: &tls.Config{RootCAs: x509.NewCertPool(), MinVersion: tls.VersionTLS12}}) if err != nil { t.Fatalf("nats connect (bad CA case): %v", err) } defer bad.Close() if _, err := bad.CreateRoom("room.tls.fail", room.ModeNATS); err == nil { t.Fatalf("a control-plane request without the CA must fail the TLS handshake") } }