towerops-agent/hostkeys_test.go
Graham McIntire aef770ea83
test: achieve 92%+ coverage with comprehensive integration and edge case tests
Cover WebSocket session lifecycle (result channels, SNMP batch flushing,
heartbeats, restart), MikroTik TLS TOFU verification mismatch, worker pool
exhaustion, plaintext WebSocket rejection, HTTP/TCP/DNS check execution,
IPv4 fallback, and customer network edge cases.

Make heartbeat intervals and toWebSocketURL exit injectable for testing.
2026-03-04 16:06:29 -06:00

135 lines
3.3 KiB
Go

package main
import (
"crypto/x509"
"path/filepath"
"sync"
"testing"
)
func TestHostKeyStoreTOFU(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "known_hosts.json")
s := newHostKeyStore(path)
// First connection should succeed (trust on first use)
if err := s.verify("10.0.0.1:22", "abc123"); err != nil {
t.Fatalf("first connect should succeed: %v", err)
}
// Same key should succeed
if err := s.verify("10.0.0.1:22", "abc123"); err != nil {
t.Fatalf("same key should succeed: %v", err)
}
// Different key should fail
if err := s.verify("10.0.0.1:22", "different"); err == nil {
t.Fatal("changed key should fail")
}
// New host should succeed
if err := s.verify("10.0.0.2:22", "def456"); err != nil {
t.Fatalf("new host should succeed: %v", err)
}
}
func TestHostKeyStorePersistence(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "known_hosts.json")
s1 := newHostKeyStore(path)
_ = s1.verify("host1:22", "fp1")
// Load from same file
s2 := newHostKeyStore(path)
if err := s2.verify("host1:22", "fp1"); err != nil {
t.Fatalf("persisted key should match: %v", err)
}
if err := s2.verify("host1:22", "changed"); err == nil {
t.Fatal("changed key should fail after reload")
}
}
func TestHostKeyStoreMissingFile(t *testing.T) {
s := newHostKeyStore("/nonexistent/path/known_hosts.json")
// Should work in memory even if file can't be read
if err := s.verify("host:22", "fp"); err != nil {
t.Fatalf("should work without file: %v", err)
}
}
func TestHostKeyStoreConcurrency(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "known_hosts.json")
s := newHostKeyStore(path)
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = s.verify("host:22", "fp1")
}()
}
wg.Wait()
}
func TestGetHostKeyStoreDefault(t *testing.T) {
// Reset the once for testing
origStore := globalHostKeys
defer func() {
hostKeysOnce = sync.Once{}
globalHostKeys = origStore
}()
hostKeysOnce = sync.Once{}
t.Setenv("TOWEROPS_HOST_KEYS_FILE", filepath.Join(t.TempDir(), "test_hosts.json"))
store := getHostKeyStore()
if store == nil {
t.Fatal("expected non-nil store")
}
}
func TestSSHHostKeyCallback(t *testing.T) {
// Reset global state
origStore := globalHostKeys
defer func() {
hostKeysOnce = sync.Once{}
globalHostKeys = origStore
}()
hostKeysOnce = sync.Once{}
t.Setenv("TOWEROPS_HOST_KEYS_FILE", filepath.Join(t.TempDir(), "hosts.json"))
cb := sshHostKeyCallback()
if cb == nil {
t.Fatal("expected non-nil callback")
}
}
func TestHostKeyStoreSaveError(t *testing.T) {
// Use a path in a non-existent directory
s := newHostKeyStore("/nonexistent/dir/hosts.json")
// verify should not error even if save fails (it logs instead)
if err := s.verify("host:22", "fp"); err != nil {
t.Fatalf("should succeed even if save fails: %v", err)
}
}
func TestTlsCertFingerprint(t *testing.T) {
cert := &x509.Certificate{
Raw: []byte("test certificate data"),
}
fp := tlsCertFingerprint(cert)
if fp == "" {
t.Error("expected non-empty fingerprint")
}
// Verify it's a hex-encoded SHA256 (64 chars)
if len(fp) != 64 {
t.Errorf("expected 64-char hex fingerprint, got %d chars: %s", len(fp), fp)
}
// Same input should produce same output
fp2 := tlsCertFingerprint(cert)
if fp != fp2 {
t.Error("fingerprint not deterministic")
}
}