status_test.go
Rama main · réplica de lectura del repositorio original.
package predastore_test
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"path/filepath"
"strconv"
"sync"
"testing"
"time"
"github.com/mulgadc/predastore"
"github.com/mulgadc/predastore/internal/config"
"github.com/mulgadc/predastore/internal/meta"
"github.com/mulgadc/predastore/internal/rpc"
"github.com/mulgadc/predastore/internal/testcerts"
"github.com/mulgadc/predastore/internal/transport"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// startReplicaOverQUIC runs one meta replica reachable over quic on
// loopback, presenting certPath/keyPath, and blocks until it has
// bootstrapped and elected itself leader. It returns the one-node cluster
// config the replica answers as.
func startReplicaOverQUIC(t *testing.T, certPath, keyPath string) *predastore.Config {
t.Helper()
quic, err := transport.NewQUICTransport("127.0.0.1", 0, certPath, keyPath)
require.NoError(t, err)
t.Cleanup(func() { quic.Close() })
ln, err := quic.Listen()
require.NoError(t, err)
_, portStr, err := net.SplitHostPort(ln.Addr().String())
require.NoError(t, err)
port, err := strconv.Atoi(portStr)
require.NoError(t, err)
cfg := &config.Config{
Hosts: []config.Host{{
ID: 1,
Addr: "127.0.0.1",
TLSCert: certPath,
TLSKey: keyPath,
Nodes: []config.Node{
{ID: 1, Role: config.RoleMeta, Port: port},
},
}},
}
res, err := rpc.NewResolver(cfg, 1, quic)
require.NoError(t, err)
leader := make(chan struct{})
svc, err := meta.New(meta.Config{
NodeID: 1,
DataDir: t.TempDir(),
Peers: []config.NodeID{1},
Bootstrap: true,
Listeners: []transport.Listener{ln},
Resolver: res,
OnLeader: sync.OnceFunc(func() { close(leader) }),
})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() { done <- svc.Run(ctx) }()
t.Cleanup(func() {
cancel()
<-done
})
select {
case <-leader:
case <-time.After(5 * time.Second):
t.Fatal("replica never elected a leader")
}
return cfg
}
// startStatusCluster is startReplicaOverQUIC with a self-signed certificate,
// returning the pool that trusts exactly it alongside the cluster config.
func startStatusCluster(t *testing.T) (*predastore.Config, *x509.CertPool) {
t.Helper()
certPath, keyPath, pool := testcerts.Generate(t)
return startReplicaOverQUIC(t, certPath, keyPath), pool
}
// TestNodeStatus_EndToEnd exercises the whole re-exported surface a caller
// outside this module has: build a Config, hand it, a node id and a trusted
// CA pool to NodeStatus, get back the raft status that Config's own node
// answers with — with no other predastore import required.
func TestNodeStatus_EndToEnd(t *testing.T) {
cfg, pool := startStatusCluster(t)
require.Equal(t, []predastore.NodeID{1}, predastore.MetaNodesOnHost(cfg, 1))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
st, err := predastore.NodeStatus(ctx, cfg, 1, pool)
require.NoError(t, err)
assert.Equal(t, "1", st.NodeID)
assert.Equal(t, "Leader", st.State)
assert.True(t, st.IsLeader)
assert.NotEmpty(t, st.Leader)
assert.NotEmpty(t, st.Term)
}
// TestNodeStatus_UnknownNode confirms a node id absent from the
// configuration is a plain error rather than a nil-pointer surprise: this is
// the mistake a caller passing the wrong id makes, not the cluster's, so it
// should read as one.
func TestNodeStatus_UnknownNode(t *testing.T) {
cfg, pool := startStatusCluster(t)
_, err := predastore.NodeStatus(context.Background(), cfg, 99, pool)
require.Error(t, err)
}
// TestMetaNodesOnHost_NoHost confirms an id naming no host is nil rather
// than an error: a host running no meta node of its own is a normal
// topology, not a mistake.
func TestMetaNodesOnHost_NoHost(t *testing.T) {
cfg, _ := startStatusCluster(t)
assert.Nil(t, predastore.MetaNodesOnHost(cfg, 99))
}
// TestNodeStatus_CrossHostCertificate is the shape a real deployment is:
// every [[host]] names the same tls_cert/tls_key path, but each host's file
// there holds a different certificate. The caller here holds neither the
// target's certificate nor its key — only the CA that issued it — and the
// target's config entry points at paths that do not exist on this machine
// at all, so the test fails outright if NodeStatus ever tries to read them.
func TestNodeStatus_CrossHostCertificate(t *testing.T) {
ca, caKey := genTestCA(t)
caPool := x509.NewCertPool()
caPool.AddCert(ca)
// A leaf distinct from anything the caller holds, signed by the shared
// CA rather than self-signed.
remoteCert, remoteKey := genTestLeaf(t, ca, caKey, "remote-meta-node")
cfg := startReplicaOverQUIC(t, remoteCert, remoteKey)
// Point the host's config entry at paths this process cannot read, to
// prove NodeStatus never touches them for a node it did not start.
cfg.Hosts[0].TLSCert = "/nonexistent/remote-host/server.pem"
cfg.Hosts[0].TLSKey = "/nonexistent/remote-host/server.key"
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
st, err := predastore.NodeStatus(ctx, cfg, 1, caPool)
require.NoError(t, err)
assert.True(t, st.IsLeader)
}
// genTestCA creates a self-signed CA cert/key pair for issuing leaf
// certificates from, distinct from testcerts.Generate's single self-signed
// leaf: a status client must verify a node's cert through a CA it did not
// itself present.
func genTestCA(t *testing.T) (*x509.Certificate, *rsa.PrivateKey) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "predastore-test-ca"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert, key
}
// genTestLeaf issues a certificate for name signed by ca/caKey, writes the
// cert and key as PEM files under t.TempDir(), and returns their paths.
func genTestLeaf(t *testing.T, ca *x509.Certificate, caKey *rsa.PrivateKey, name string) (certPath, keyPath string) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: name},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, ca, &key.PublicKey, caKey)
require.NoError(t, err)
dir := t.TempDir()
certPath = filepath.Join(dir, "leaf.pem")
keyPath = filepath.Join(dir, "leaf.key")
require.NoError(t, os.WriteFile(certPath,
pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600))
require.NoError(t, os.WriteFile(keyPath,
pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}), 0o600))
return certPath, keyPath
}