Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
92 lines
2.7 KiB
Go
92 lines
2.7 KiB
Go
package loadbalancer
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/owen/vibeproxy/internal/models"
|
|
)
|
|
|
|
func TestRoundRobin(t *testing.T) {
|
|
b1 := NewBackend(models.Backend{Host: "a", Enabled: true, HealthCheckType: models.HealthCheckNone})
|
|
b2 := NewBackend(models.Backend{Host: "b", Enabled: true, HealthCheckType: models.HealthCheckNone})
|
|
pool := NewPool("round_robin", []*Backend{b1, b2})
|
|
|
|
seen := map[string]int{}
|
|
for i := 0; i < 10; i++ {
|
|
b, err := pool.Pick()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen[b.Model.Host]++
|
|
}
|
|
if seen["a"] != 5 || seen["b"] != 5 {
|
|
t.Fatalf("expected even split, got %v", seen)
|
|
}
|
|
}
|
|
|
|
func TestLeastConn(t *testing.T) {
|
|
b1 := NewBackend(models.Backend{Host: "a", Enabled: true, HealthCheckType: models.HealthCheckNone})
|
|
b2 := NewBackend(models.Backend{Host: "b", Enabled: true, HealthCheckType: models.HealthCheckNone})
|
|
pool := NewPool("least_conn", []*Backend{b1, b2})
|
|
|
|
b1.Acquire()
|
|
b1.Acquire()
|
|
picked, err := pool.Pick()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if picked.Model.Host != "b" {
|
|
t.Fatalf("expected b (fewer active conns), got %s", picked.Model.Host)
|
|
}
|
|
}
|
|
|
|
func TestWeighted(t *testing.T) {
|
|
b1 := NewBackend(models.Backend{Host: "a", Weight: 3, Enabled: true, HealthCheckType: models.HealthCheckNone})
|
|
b2 := NewBackend(models.Backend{Host: "b", Weight: 1, Enabled: true, HealthCheckType: models.HealthCheckNone})
|
|
pool := NewPool("weighted", []*Backend{b1, b2})
|
|
|
|
counts := map[string]int{}
|
|
for i := 0; i < 8; i++ {
|
|
b, err := pool.Pick()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
counts[b.Model.Host]++
|
|
}
|
|
if counts["a"] != 6 || counts["b"] != 2 {
|
|
t.Fatalf("expected 3:1 weighted split (6:2 of 8), got %v", counts)
|
|
}
|
|
}
|
|
|
|
func TestNoHealthyBackend(t *testing.T) {
|
|
b1 := NewBackend(models.Backend{Host: "a", Enabled: true, HealthCheckType: models.HealthCheckTCP, FailureThreshold: 1})
|
|
pool := NewPool("round_robin", []*Backend{b1})
|
|
if _, err := pool.Pick(); err != ErrNoHealthyBackend {
|
|
t.Fatalf("expected ErrNoHealthyBackend, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRecordCheckThresholds(t *testing.T) {
|
|
b := NewBackend(models.Backend{Host: "a", Enabled: true, HealthCheckType: models.HealthCheckTCP, FailureThreshold: 2, RecoveryThreshold: 2})
|
|
if b.Healthy() {
|
|
t.Fatal("expected backend to start unhealthy pending checks")
|
|
}
|
|
b.RecordCheck(true)
|
|
if b.Healthy() {
|
|
t.Fatal("should still be unhealthy after 1 of 2 required successes")
|
|
}
|
|
changed, healthy := b.RecordCheck(true)
|
|
if !changed || !healthy {
|
|
t.Fatal("expected backend to become healthy after 2 successes")
|
|
}
|
|
|
|
b.RecordCheck(false)
|
|
if !b.Healthy() {
|
|
t.Fatal("should still be healthy after 1 of 2 required failures")
|
|
}
|
|
changed, healthy = b.RecordCheck(false)
|
|
if !changed || healthy {
|
|
t.Fatal("expected backend to become unhealthy after 2 failures")
|
|
}
|
|
}
|