package workercluster import ( "net" "net/http" "net/http/httptest" "testing" rafthttp "github.com/CanonicalLtd/raft-http" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // newLocalListener opens a TCP listener on the loopback interface // using an ephemeral port. Used by transport and e2e tests. func newLocalListener(t *testing.T) net.Listener { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) return l } // TestTransport_Auth_RejectsMissingCreds verifies that a request // without an Authorization header is rejected with 401. func TestTransport_Auth_RejectsMissingCreds(t *testing.T) { h := rafthttp.NewHandler() wrapped := NewAuthHandler(h, HTTPCreds{Login: "alice", Password: "secret"}, nil) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/raft", nil) wrapped.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) assert.Equal(t, `Basic realm="raft"`, rec.Header().Get("WWW-Authenticate")) } // TestTransport_Auth_RejectsWrongCreds verifies that a request with // incorrect credentials is rejected with 401. func TestTransport_Auth_RejectsWrongCreds(t *testing.T) { h := rafthttp.NewHandler() wrapped := NewAuthHandler(h, HTTPCreds{Login: "alice", Password: "secret"}, nil) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/raft", nil) req.SetBasicAuth("alice", "wrong") wrapped.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) } // TestTransport_Auth_AcceptsCorrectCreds verifies that a request with // matching credentials is forwarded to the inner rafthttp handler. The // rafthttp GET path expects an Upgrade header; without one it returns // 400, but the important point is that the auth wrapper does not block // the request before reaching the inner handler. func TestTransport_Auth_AcceptsCorrectCreds(t *testing.T) { h := rafthttp.NewHandler() wrapped := NewAuthHandler(h, HTTPCreds{Login: "alice", Password: "secret"}, nil) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/raft", nil) req.SetBasicAuth("alice", "secret") wrapped.ServeHTTP(rec, req) assert.NotEqual(t, http.StatusUnauthorized, rec.Code, "auth wrapper should not block good credentials") assert.Equal(t, http.StatusBadRequest, rec.Code, "rafthttp expects Upgrade:raft header; with auth, 400 confirms we reached the inner handler") } // TestTransport_Auth_PanicsOnEmptyCreds verifies the wrapper refuses // to construct without both login and password. func TestTransport_Auth_PanicsOnEmptyCreds(t *testing.T) { assert.Panics(t, func() { NewAuthHandler(rafthttp.NewHandler(), HTTPCreds{Login: "", Password: ""}, nil) }) } // TestTransport_Auth_TimingSafe verifies that the wrapper uses // crypto/subtle.ConstantTimeCompare rather than ==. We assert this // indirectly: with wrong creds the response is always 401 regardless of // how close the password is to the real one. func TestTransport_Auth_TimingSafe(t *testing.T) { h := rafthttp.NewHandler() wrapped := NewAuthHandler(h, HTTPCreds{Login: "alice", Password: "secret"}, nil) for _, pw := range []string{"s", "se", "sec", "secr", "secre", "secret", "secretX"} { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/raft", nil) req.SetBasicAuth("alice", pw) wrapped.ServeHTTP(rec, req) if pw == "secret" { assert.NotEqual(t, http.StatusUnauthorized, rec.Code, "exact password should pass auth") } else { assert.Equal(t, http.StatusUnauthorized, rec.Code, "password %q should fail auth", pw) } } } // TestTransport_HTTPCreds_IsConfigured verifies the IsConfigured // contract: both set, or neither. func TestTransport_HTTPCreds_IsConfigured(t *testing.T) { cases := []struct { creds HTTPCreds want bool }{ {HTTPCreds{Login: "u", Password: "p"}, true}, {HTTPCreds{Login: "u"}, false}, {HTTPCreds{Password: "p"}, false}, {HTTPCreds{}, false}, } for _, tc := range cases { assert.Equal(t, tc.want, tc.creds.IsConfigured(), "%+v", tc.creds) } } // TestTransport_NewTransportRequiresArgs guards the constructor's // invariants. func TestTransport_NewTransportRequiresArgs(t *testing.T) { h := rafthttp.NewHandler() l := newLocalListener(t) _, _, err := NewTransport("", l, nil, nil, nil) require.Error(t, err) _, _, err = NewTransport("", nil, h, nil, nil) require.Error(t, err) layer, srv, err := NewTransport("/raft", l, h, nil, nil) require.NoError(t, err) require.NotNil(t, layer) require.NotNil(t, srv) require.NoError(t, srv.Close()) require.NoError(t, l.Close()) }