Ctrl K
ComunidadGuías…

status_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}