Initial vibeproxy implementation

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
This commit is contained in:
2026-08-17 10:28:59 -05:00
co-authored by Claude Sonnet 5
commit ba1f7c4c03
105 changed files with 9142 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
package database
import (
"database/sql"
"path/filepath"
"testing"
"github.com/owen/vibeproxy/internal/models"
)
func openTest(t *testing.T) *sql.DB {
t.Helper()
db, err := Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
return db
}
func TestMigrateAndProxyHostRoundTrip(t *testing.T) {
db := openTest(t)
h := models.ProxyHost{
Name: "test", Type: models.ProxyHostTypeProxy, Domains: []string{"a.example.com", "b.example.com"},
ForwardScheme: "http", LoadBalancingMethod: "round_robin", Enabled: true,
ConnectTimeoutMS: 5000, ReadTimeoutMS: 60000, WriteTimeoutMS: 60000, IdleTimeoutMS: 120000,
Backends: []models.Backend{
{Host: "10.0.0.1", Port: 8080, Weight: 1, Enabled: true, HealthCheckType: models.HealthCheckTCP, IntervalSeconds: 10, FailureThreshold: 3, RecoveryThreshold: 2},
},
CustomHeaders: []models.CustomHeader{
{Direction: models.HeaderDirectionResponse, Action: models.HeaderActionSet, Name: "X-Frame-Options", Value: "DENY"},
},
}
id, err := SaveProxyHost(db, &h)
if err != nil {
t.Fatal(err)
}
if id == 0 {
t.Fatal("expected non-zero id")
}
fetched, err := GetProxyHost(db, id)
if err != nil {
t.Fatal(err)
}
if fetched.Name != "test" || len(fetched.Domains) != 2 || len(fetched.Backends) != 1 || len(fetched.CustomHeaders) != 1 {
t.Fatalf("round trip mismatch: %+v", fetched)
}
// Update should replace domains/backends/headers cleanly.
fetched.Domains = []string{"only.example.com"}
fetched.Backends[0].Port = 9090
if _, err := SaveProxyHost(db, fetched); err != nil {
t.Fatal(err)
}
updated, err := GetProxyHost(db, id)
if err != nil {
t.Fatal(err)
}
if len(updated.Domains) != 1 || updated.Domains[0] != "only.example.com" {
t.Fatalf("expected domains replaced, got %v", updated.Domains)
}
if updated.Backends[0].Port != 9090 {
t.Fatalf("expected backend port updated, got %d", updated.Backends[0].Port)
}
if err := DeleteProxyHost(db, id); err != nil {
t.Fatal(err)
}
if _, err := GetProxyHost(db, id); err != ErrNotFound {
t.Fatalf("expected ErrNotFound after delete, got %v", err)
}
}
func TestSettingsRoundTrip(t *testing.T) {
db := openTest(t)
if err := SetSetting(db, "acme_email", "ops@example.com"); err != nil {
t.Fatal(err)
}
if got := GetSetting(db, "acme_email", ""); got != "ops@example.com" {
t.Fatalf("expected ops@example.com, got %q", got)
}
if got := GetSetting(db, "missing_key", "fallback"); got != "fallback" {
t.Fatalf("expected fallback default, got %q", got)
}
}