mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
chore: add new tests
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user