Files
kportal/internal/forward/worker_test.go
T
lukaszraczylo 23cd45a3d7 improvements nov2025 pt2 (#13)
* Further improvements

| Fix                                | Impact                                 | Files Modified                       |
|------------------------------------|----------------------------------------|--------------------------------------|
| sync.Pool for health check buffers | Reduces GC pressure ~30%               | internal/healthcheck/checker.go      |
| Goroutine leak fix + sync.Once     | Prevents memory leaks                  | internal/forward/worker.go           |
| Cache eviction for expired entries | Prevents unbounded memory growth       | internal/k8s/resolver.go             |
| Backoff reset on success           | Faster recovery after long connections | internal/forward/worker.go           |
| Converter file permissions         | Security hardening (0644→0600)         | internal/converter/kftray.go         |
| HTTP body size limiting            | Prevents OOM with large requests       | internal/httplog/proxy.go, logger.go |
| WaitGroup for config watcher       | Clean goroutine shutdown               | internal/config/watcher.go           |
| Signal handler cleanup             | Ensures all resources released         | cmd/kportal/main.go                  |

* Additional event bus for internal event handling

| Metric                 | Before                                | After             | Improvement        |
|------------------------|---------------------------------------|-------------------|--------------------|
| Goroutines per forward | 3 (worker + heartbeat + health check) | 1 (worker only)   | 66% reduction      |
| Tickers per forward    | 2 (heartbeat + health check)          | 0                 | 100% reduction     |
| Global goroutines      | 2 (watchdog + health monitor)         | 2                 | Same               |
| Lock acquisitions/sec  | O(n) per interval                     | O(1) per interval | Linear improvement |


* Add UI testing
* Add mocks
* Add more logs and details to be displayed
2025-11-26 13:18:50 +00:00

354 lines
8.5 KiB
Go

package forward
import (
"context"
"testing"
"time"
"github.com/nvm/kportal/internal/config"
"github.com/stretchr/testify/assert"
)
// TestNewForwardWorker tests worker creation
func TestNewForwardWorker(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
assert.NotNil(t, worker)
assert.Equal(t, fwd, worker.forward)
assert.False(t, worker.verbose)
assert.NotNil(t, worker.ctx)
assert.NotNil(t, worker.stopChan)
assert.NotNil(t, worker.doneChan)
assert.NotNil(t, worker.reconnectChan)
assert.NotNil(t, worker.successChan)
}
// TestNewForwardWorker_Verbose tests verbose mode worker creation
func TestNewForwardWorker_Verbose(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, true, nil, nil, nil)
assert.True(t, worker.verbose)
}
// TestWorker_GetForwardConfig tests getting forward config
func TestWorker_GetForwardConfig(t *testing.T) {
fwd := config.Forward{
Resource: "service/postgres",
LocalPort: 5432,
Port: 5432,
Alias: "db",
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
result := worker.GetForward()
assert.Equal(t, fwd, result)
assert.Equal(t, "service/postgres", result.Resource)
assert.Equal(t, 5432, result.LocalPort)
assert.Equal(t, "db", result.Alias)
}
// TestForwardWorker_GetForwardID tests GetForwardID implementation
func TestForwardWorker_GetForwardID(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
id := worker.GetForwardID()
assert.NotEmpty(t, id)
assert.Equal(t, fwd.ID(), id)
}
// TestForwardWorker_IsAlive tests IsAlive implementation
func TestForwardWorker_IsAlive(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Before starting, worker should be "alive" (context not cancelled)
assert.True(t, worker.IsAlive())
// Cancel context
worker.cancel()
// After cancel, IsAlive should return false
assert.False(t, worker.IsAlive())
}
// TestWorker_IsRunningState tests IsRunning method
func TestWorker_IsRunningState(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Before done channel is closed, worker is "running"
assert.True(t, worker.IsRunning())
// Close done channel to simulate worker completion
close(worker.doneChan)
// After done channel closed, worker is not running
assert.False(t, worker.IsRunning())
}
// TestForwardWorker_SignalConnectionSuccess tests success signaling
func TestForwardWorker_SignalConnectionSuccess(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Signal success
worker.signalConnectionSuccess()
// Should be able to receive from success channel
select {
case <-worker.successChan:
// Success
case <-time.After(100 * time.Millisecond):
t.Fatal("Expected signal on success channel")
}
// Second signal should not block (buffer of 1)
worker.signalConnectionSuccess()
worker.signalConnectionSuccess() // Should not block
// Channel should have at most 1 pending signal
select {
case <-worker.successChan:
// Got the signal
default:
// No signal (also acceptable - channel already had one)
}
}
// TestForwardWorker_TriggerReconnect tests reconnect triggering
func TestForwardWorker_TriggerReconnect(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Trigger reconnect
worker.TriggerReconnect("test reason")
// Should be able to receive from reconnect channel
select {
case reason := <-worker.reconnectChan:
assert.Equal(t, "test reason", reason)
case <-time.After(100 * time.Millisecond):
t.Fatal("Expected signal on reconnect channel")
}
}
// TestForwardWorker_TriggerReconnect_WithForwardCancel tests reconnect with active forward
func TestForwardWorker_TriggerReconnect_WithForwardCancel(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Set up a forward cancel function
ctx, cancel := context.WithCancel(context.Background())
worker.forwardCancelMu.Lock()
worker.forwardCancel = cancel
worker.forwardCancelMu.Unlock()
// Trigger reconnect
worker.TriggerReconnect("stale connection")
// Context should be cancelled
select {
case <-ctx.Done():
// Success - context was cancelled
case <-time.After(100 * time.Millisecond):
t.Fatal("Expected forward context to be cancelled")
}
}
// TestForwardWorker_TriggerReconnect_NonBlocking tests non-blocking behavior
func TestForwardWorker_TriggerReconnect_NonBlocking(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Fill the channel
worker.reconnectChan <- "first"
// Second trigger should not block
done := make(chan bool)
go func() {
worker.TriggerReconnect("second")
done <- true
}()
select {
case <-done:
// Success - didn't block
case <-time.After(100 * time.Millisecond):
t.Fatal("TriggerReconnect blocked when channel was full")
}
}
// TestForwardWorker_Stop tests graceful stop
func TestForwardWorker_Stop(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Close done channel to simulate worker has finished
close(worker.doneChan)
// Stop should complete quickly since worker is "done"
done := make(chan bool)
go func() {
worker.Stop()
done <- true
}()
select {
case <-done:
// Success
case <-time.After(5 * time.Second):
t.Fatal("Stop timed out")
}
}
// TestForwardWorker_Stop_Timeout tests stop timeout behavior
func TestForwardWorker_Stop_Timeout(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Don't close doneChan - simulate hanging worker
// Stop should timeout after ~3 seconds
start := time.Now()
done := make(chan bool)
go func() {
worker.Stop()
done <- true
}()
select {
case <-done:
elapsed := time.Since(start)
// Should have waited at least 2 seconds but not more than 5
assert.True(t, elapsed >= 2*time.Second, "Should wait for timeout")
assert.True(t, elapsed < 5*time.Second, "Should not wait too long")
case <-time.After(10 * time.Second):
t.Fatal("Stop never completed")
}
}
// TestForwardWorker_GetHTTPProxy tests HTTP proxy getter
func TestForwardWorker_GetHTTPProxy(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Initially nil
proxy := worker.GetHTTPProxy()
assert.Nil(t, proxy)
}
// TestForwardWorker_HeartbeatResponder tests HeartbeatResponder interface
func TestForwardWorker_HeartbeatResponder(t *testing.T) {
fwd := config.Forward{
Resource: "pod/my-app",
LocalPort: 8080,
Port: 80,
}
worker := NewForwardWorker(fwd, nil, false, nil, nil, nil)
// Worker should implement HeartbeatResponder
var responder HeartbeatResponder = worker
assert.NotNil(t, responder)
// Test interface methods
assert.True(t, responder.IsAlive())
assert.NotEmpty(t, responder.GetForwardID())
}
// TestLogWriter tests the logWriter implementation
func TestLogWriter(t *testing.T) {
tests := []struct {
name string
prefix string
input []byte
}{
{"simple message", "[test] ", []byte("hello")},
{"empty message", "[test] ", []byte("")},
{"multiline", "[test] ", []byte("line1\nline2")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lw := &logWriter{prefix: tt.prefix}
n, err := lw.Write(tt.input)
assert.NoError(t, err)
assert.Equal(t, len(tt.input), n)
})
}
}
// TestHTTPLogPortOffset tests the port offset constant
func TestHTTPLogPortOffset(t *testing.T) {
assert.Equal(t, 10000, httpLogPortOffset)
}
// TestPortForwardReadyTimeout tests the ready timeout constant
func TestPortForwardReadyTimeout(t *testing.T) {
assert.Equal(t, 30*time.Second, portForwardReadyTimeout)
}