chore: add new tests

This commit is contained in:
Julian Sommer
2026-05-13 11:02:03 +10:00
parent 4e5d7b0a13
commit 352eb164c1
5 changed files with 333 additions and 1 deletions
+7 -1
View File
@@ -6,13 +6,15 @@ import (
"github.com/mulgadc/spinifex/spinifex/config"
"github.com/mulgadc/spinifex/spinifex/services/nats"
"github.com/mulgadc/spinifex/spinifex/services/predastore"
"github.com/mulgadc/spinifex/spinifex/services/spinifexui"
"github.com/mulgadc/spinifex/spinifex/services/viperblockd"
"github.com/mulgadc/spinifex/spinifex/services/vpcd"
"github.com/stretchr/testify/assert"
)
func TestNew(t *testing.T) {
// Test known service types
services := []string{"nats", "predastore", "viperblock", "spinifex", "awsgw"}
services := []string{"nats", "predastore", "viperblock", "spinifex", "awsgw", "spinifex-ui", "vpcd"}
for _, s := range services {
var svc Service
@@ -32,6 +34,10 @@ func TestNew(t *testing.T) {
case "awsgw":
svc, err = New(s, &config.ClusterConfig{})
// No special setup needed
case "spinifex-ui":
svc, err = New(s, &spinifexui.Config{})
case "vpcd":
svc, err = New(s, &vpcd.Config{})
}
assert.NoError(t, err)
+15
View File
@@ -119,3 +119,18 @@ func TestRenderedConfig_HasMigrationVersionMarker(t *testing.T) {
assert.Equal(t, "# spinifex-config-version: 3", firstLine,
"nats.conf template must start with the current migration version marker")
}
func TestNew_ValidConfig(t *testing.T) {
cfg := &Config{Port: 4222, Host: "127.0.0.1"}
svc, err := New(cfg)
require.NoError(t, err)
require.NotNil(t, svc)
assert.Same(t, cfg, svc.Config)
}
func TestNew_InvalidConfigType(t *testing.T) {
svc, err := New("not a config")
assert.Nil(t, svc)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid config type")
}
@@ -0,0 +1,223 @@
package spinifexui
import (
"bufio"
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"io"
"math/big"
"net"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPrefixConn_Read_ReplaysPrefixThenStream(t *testing.T) {
server, client := net.Pipe()
defer server.Close()
defer client.Close()
go func() {
_, _ = client.Write([]byte("est"))
}()
pc := &prefixConn{
Conn: server,
r: io.MultiReader(bytes.NewReader([]byte{'t'}), server),
}
buf := make([]byte, 4)
n, err := io.ReadFull(pc, buf)
require.NoError(t, err)
assert.Equal(t, 4, n)
assert.Equal(t, "test", string(buf))
}
// startSplitListener returns a tlsSplitListener on a random local port. Caller
// should close the returned net.Listener when done. The Accept loop runs in a
// goroutine and accepted TLS connections are pushed onto the returned channel.
func startSplitListener(t *testing.T, tlsCfg *tls.Config, redirectPort int) (*tlsSplitListener, net.Listener, <-chan net.Conn) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
splitLn := &tlsSplitListener{
Listener: ln,
port: redirectPort,
tlsCfg: tlsCfg,
}
accepted := make(chan net.Conn, 1)
go func() {
defer close(accepted)
for {
c, err := splitLn.Accept()
if err != nil {
return
}
accepted <- c
}
}()
return splitLn, ln, accepted
}
func TestTLSSplitListener_Accept_TLSHandshakeByteWrapsTLS(t *testing.T) {
cert := loadTLSCert(t)
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
_, ln, accepted := startSplitListener(t, tlsCfg, 8443)
defer ln.Close()
conn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer conn.Close()
// First byte 0x16 = TLS ClientHello record type — listener should wrap with TLS
// without yet performing the handshake.
_, err = conn.Write([]byte{0x16})
require.NoError(t, err)
select {
case c := <-accepted:
require.NotNil(t, c)
defer c.Close()
_, ok := c.(*tls.Conn)
assert.True(t, ok, "expected *tls.Conn, got %T", c)
case <-time.After(2 * time.Second):
t.Fatal("Accept did not return a TLS connection in time")
}
}
func TestTLSSplitListener_Accept_PlainHTTPRedirectsToHTTPS(t *testing.T) {
_, ln, _ := startSplitListener(t, &tls.Config{}, 8443)
defer ln.Close()
conn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer conn.Close()
_, err = conn.Write([]byte("GET /foo?x=1 HTTP/1.1\r\nHost: example.com\r\n\r\n"))
require.NoError(t, err)
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusMovedPermanently, resp.StatusCode)
assert.Equal(t, "https://example.com:8443/foo?x=1", resp.Header.Get("Location"))
}
func TestTLSSplitListener_Accept_PlainHTTPStripsHostPort(t *testing.T) {
_, ln, _ := startSplitListener(t, &tls.Config{}, 9443)
defer ln.Close()
conn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer conn.Close()
// Host with explicit port — redirectHTTP should strip it before re-emitting.
_, err = conn.Write([]byte("GET /a HTTP/1.1\r\nHost: example.com:80\r\n\r\n"))
require.NoError(t, err)
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, "https://example.com:9443/a", resp.Header.Get("Location"))
}
func TestTLSSplitListener_Accept_MalformedHTTPClosesSilently(t *testing.T) {
_, ln, _ := startSplitListener(t, &tls.Config{}, 8443)
defer ln.Close()
conn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer conn.Close()
// Non-TLS first byte but garbage afterwards — http.ReadRequest fails and
// redirectHTTP returns without writing a response.
_, err = conn.Write([]byte("NOT-AN-HTTP-REQUEST\r\n\r\n"))
require.NoError(t, err)
require.NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second)))
buf := make([]byte, 256)
n, _ := conn.Read(buf)
assert.Equal(t, 0, n, "no response expected for malformed request")
}
func TestTLSSplitListener_Accept_ClientClosesBeforeFirstByte(t *testing.T) {
cert := loadTLSCert(t)
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
_, ln, accepted := startSplitListener(t, tlsCfg, 8443)
defer ln.Close()
// Dial and immediately close — listener should drop the conn and keep looping.
c, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
require.NoError(t, c.Close())
// Now send a valid TLS first byte from a fresh connection to verify the
// loop still serves subsequent clients.
c2, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer c2.Close()
_, err = c2.Write([]byte{0x16})
require.NoError(t, err)
select {
case got := <-accepted:
require.NotNil(t, got)
defer got.Close()
_, ok := got.(*tls.Conn)
assert.True(t, ok)
case <-time.After(2 * time.Second):
t.Fatal("Accept loop did not recover after client disconnect")
}
}
// loadTLSCert builds a self-signed certificate for local listener tests.
func loadTLSCert(t *testing.T) tls.Certificate {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "tlssplit-test"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyDER, err := x509.MarshalECPrivateKey(key)
require.NoError(t, err)
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
cert, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
return cert
}
+18
View File
@@ -34,3 +34,21 @@ func TestExtractTags(t *testing.T) {
assert.NotNil(t, empty)
assert.Empty(t, empty)
}
func TestMapToEC2Tags_Populated(t *testing.T) {
got := MapToEC2Tags(map[string]string{
"Name": "web-1",
"Env": "prod",
})
assert.Len(t, got, 2)
asMap := make(map[string]string, len(got))
for _, tag := range got {
asMap[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value)
}
assert.Equal(t, map[string]string{"Name": "web-1", "Env": "prod"}, asMap)
}
func TestMapToEC2Tags_Empty(t *testing.T) {
assert.Nil(t, MapToEC2Tags(nil))
assert.Nil(t, MapToEC2Tags(map[string]string{}))
}
+70
View File
@@ -1004,6 +1004,76 @@ func TestReadPidFileFrom_EmptyDir(t *testing.T) {
assert.Error(t, err)
}
func TestServiceStatus_Stopped(t *testing.T) {
dir := t.TempDir()
status, err := ServiceStatus(dir, fmt.Sprintf("no-such-svc-%d", time.Now().UnixNano()))
require.NoError(t, err)
assert.Equal(t, "stopped", status)
}
func TestServiceStatus_Running(t *testing.T) {
dir := t.TempDir()
cmd := exec.Command("sleep", "60")
require.NoError(t, cmd.Start())
defer func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}()
require.NoError(t, WritePidFileTo(dir, "running-svc", cmd.Process.Pid))
status, err := ServiceStatus(dir, "running-svc")
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("running (pid: %d)", cmd.Process.Pid), status)
}
func TestServiceStatus_CorruptPidFile(t *testing.T) {
dir := t.TempDir()
// Write a non-numeric pid file — ReadPidFileFrom should fail to parse it.
require.NoError(t, os.WriteFile(filepath.Join(dir, "bad-svc.pid"), []byte("not-a-pid"), 0o644))
_, err := ServiceStatus(dir, "bad-svc")
assert.Error(t, err)
}
func TestWaitForProcessExit_ProcessAlreadyDead(t *testing.T) {
cmd := exec.Command("true")
require.NoError(t, cmd.Start())
pid := cmd.Process.Pid
require.NoError(t, cmd.Wait())
// `true` has exited and been reaped — WaitForProcessExit should return
// nil immediately on the first tick.
err := WaitForProcessExit(pid, 2*time.Second)
assert.NoError(t, err)
}
func TestWaitForProcessExit_ProcessExitsBeforeTimeout(t *testing.T) {
cmd := exec.Command("sleep", "0.1")
require.NoError(t, cmd.Start())
pid := cmd.Process.Pid
// Reap in the background so the kernel releases the PID once sleep exits.
go func() { _ = cmd.Wait() }()
err := WaitForProcessExit(pid, 5*time.Second)
assert.NoError(t, err)
}
func TestWaitForProcessExit_Timeout(t *testing.T) {
cmd := exec.Command("sleep", "30")
require.NoError(t, cmd.Start())
defer func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}()
err := WaitForProcessExit(cmd.Process.Pid, 200*time.Millisecond)
require.Error(t, err)
assert.Contains(t, err.Error(), "timeout")
}
func TestHashMAC_FirstOctetPinnedTo02(t *testing.T) {
// First octet must be literal 0x02 across all inputs. Anything else
// (e.g. 0x4a, 0xfe — technically valid LAA) visually encroaches on