Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions internal/backend/postgres/backend_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package postgres

// backend_factory_test.go — unit tests for the NewBackend / NewDedicatedBackend
// factory in backend.go. These functions are pure construction (no live
// dependencies) so the tests run in every coverage mode.

import (
"os"
"path/filepath"
"testing"
)

func TestK8sEnv(t *testing.T) {
t.Setenv("K8S_PROBE_TEST_SET", "found")
if got := k8sEnv("K8S_PROBE_TEST_SET", "fallback"); got != "found" {
t.Errorf("k8sEnv(set) = %q; want found", got)
}
os.Unsetenv("K8S_PROBE_TEST_UNSET")
if got := k8sEnv("K8S_PROBE_TEST_UNSET", "fallback"); got != "fallback" {
t.Errorf("k8sEnv(unset) = %q; want fallback", got)
}
}

func TestK8sEnvInt(t *testing.T) {
t.Setenv("K8S_PROBE_INT_SET", "42")
if got := k8sEnvInt("K8S_PROBE_INT_SET", 9); got != 42 {
t.Errorf("k8sEnvInt(set) = %d; want 42", got)
}
t.Setenv("K8S_PROBE_INT_GARBAGE", "not-a-number")
if got := k8sEnvInt("K8S_PROBE_INT_GARBAGE", 9); got != 9 {
t.Errorf("k8sEnvInt(garbage) = %d; want fallback 9", got)
}
os.Unsetenv("K8S_PROBE_INT_UNSET")
if got := k8sEnvInt("K8S_PROBE_INT_UNSET", 9); got != 9 {
t.Errorf("k8sEnvInt(unset) = %d; want fallback 9", got)
}
}

func TestNewBackend_DefaultIsLocal(t *testing.T) {
b := NewBackend("", "postgres://u:p@h/d", "", "", "")
if _, ok := b.(*LocalBackend); !ok {
t.Errorf("NewBackend(\"\") = %T; want *LocalBackend", b)
}
}

func TestNewBackend_UnknownTypeIsLocal(t *testing.T) {
b := NewBackend("nonsense", "postgres://u:p@h/d", "", "", "")
if _, ok := b.(*LocalBackend); !ok {
t.Errorf("NewBackend(\"nonsense\") = %T; want *LocalBackend", b)
}
}

func TestNewBackend_Neon(t *testing.T) {
b := NewBackend("neon", "", "", "api-key", "us-east-1")
if _, ok := b.(*NeonBackend); !ok {
t.Errorf("NewBackend(\"neon\") = %T; want *NeonBackend", b)
}
}

func TestNewBackend_MultiCluster(t *testing.T) {
b := NewBackend("", "", "postgres://a/x, postgres://b/y , ", "", "")
lb, ok := b.(*LocalBackend)
if !ok {
t.Fatalf("NewBackend(multi) = %T; want *LocalBackend", b)
}
if got := len(lb.router.adminURLs); got != 2 {
t.Errorf("router has %d clusters; want 2 (trailing/empty entries filtered)", got)
}
}

func TestNewBackend_MultiCluster_AllEmptyFallsBack(t *testing.T) {
b := NewBackend("", "postgres://u:p@h/d", " , , ", "", "")
lb, ok := b.(*LocalBackend)
if !ok {
t.Fatalf("NewBackend(all-empty cluster URLs) = %T; want *LocalBackend", b)
}
if got := len(lb.router.adminURLs); got != 1 {
t.Errorf("router has %d clusters; want 1 single-customer URL fallback", got)
}
}

func TestNewBackend_K8s_FallsBackToLocal_WhenOutOfCluster(t *testing.T) {
// Without a valid kubeconfig or in-cluster service account, newK8sBackend
// errors and NewBackend logs + falls back to LocalBackend. We verify the
// fallback type so the gRPC server doesn't crash on dev machines.
t.Setenv("K8S_KUBECONFIG", "/nonexistent/kubeconfig-for-test")
b := NewBackend("k8s", "postgres://u:p@h/d", "", "", "")
if _, ok := b.(*LocalBackend); !ok {
t.Errorf("NewBackend(\"k8s\") with bad kubeconfig = %T; want LocalBackend fallback", b)
}
}

// TestNewBackend_K8s_RouteRegistry covers the k8s success branch of NewBackend:
// a valid kubeconfig lets newK8sBackend succeed, and REDIS_URL_FOR_ROUTES drives
// the route-registry enablement block (goredisParseURL → goredisNewClient →
// EnableRouteRegistry). Returns a *K8sBackend, not the LocalBackend fallback.
func TestNewBackend_K8s_RouteRegistry(t *testing.T) {
dir := t.TempDir()
kc := filepath.Join(dir, "kubeconfig")
if err := os.WriteFile(kc, []byte(validKubeconfig), 0o600); err != nil {
t.Fatalf("write kubeconfig: %v", err)
}
t.Setenv("K8S_KUBECONFIG", kc)
t.Setenv("REDIS_URL_FOR_ROUTES", "redis://127.0.0.1:6379/0")
t.Setenv("PG_PROXY_ROUTE_PREFIX", "test_pg_route:")
b := NewBackend("k8s", "postgres://u:p@h/d", "", "", "")
kb, ok := b.(*K8sBackend)
if !ok {
t.Fatalf("NewBackend(k8s, valid kubeconfig) = %T; want *K8sBackend", b)
}
if kb.rdb == nil {
t.Error("route registry not enabled; rdb is nil")
}
if kb.routePrefix != "test_pg_route:" {
t.Errorf("routePrefix = %q; want test_pg_route:", kb.routePrefix)
}
}

// TestNewBackend_K8s_BadRedisURL covers the route-registry-disabled branch:
// newK8sBackend succeeds but the REDIS URL fails to parse, so route registry is
// left disabled (the goredisParseURL error arm) and a *K8sBackend is still
// returned.
func TestNewBackend_K8s_BadRedisURL(t *testing.T) {
dir := t.TempDir()
kc := filepath.Join(dir, "kubeconfig")
if err := os.WriteFile(kc, []byte(validKubeconfig), 0o600); err != nil {
t.Fatalf("write kubeconfig: %v", err)
}
t.Setenv("K8S_KUBECONFIG", kc)
t.Setenv("REDIS_URL_FOR_ROUTES", "::not a redis url::")
b := NewBackend("k8s", "postgres://u:p@h/d", "", "", "")
kb, ok := b.(*K8sBackend)
if !ok {
t.Fatalf("NewBackend(k8s, bad redis URL) = %T; want *K8sBackend", b)
}
if kb.rdb != nil {
t.Error("route registry enabled despite bad redis URL; rdb should be nil")
}
}

func TestNewDedicatedBackend(t *testing.T) {
b := NewDedicatedBackend("postgres://a/x", "")
if b == nil {
t.Fatal("NewDedicatedBackend returned nil")
}
if _, ok := b.(*DedicatedProvider); !ok {
t.Errorf("NewDedicatedBackend = %T; want *DedicatedProvider", b)
}
}

func TestNewK8sDedicatedBackend_BadConfig(t *testing.T) {
_, err := NewK8sDedicatedBackend("/nonexistent/kubeconfig", "", "", "", 0)
if err == nil {
t.Error("NewK8sDedicatedBackend(bad path) returned nil; want error")
}
}
120 changes: 120 additions & 0 deletions internal/backend/postgres/cluster_router_live_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package postgres

// cluster_router_live_test.go — live-Postgres coverage for the
// ClusterRouter background-polling path (dbCount + refreshCounts) and the
// remaining pure-function helpers (ProviderResourceID).

import (
"context"
"testing"
"time"
)

func TestClusterRouter_ProviderResourceID(t *testing.T) {
r := newClusterRouter([]string{"a", "b", "c"}, 0)
for i, want := range []string{"local:0", "local:1", "local:2"} {
if got := r.ProviderResourceID(i); got != want {
t.Errorf("ProviderResourceID(%d) = %q; want %q", i, got, want)
}
}
}

func TestClusterRouter_DBCount_ConnectError(t *testing.T) {
r := newClusterRouter([]string{"postgres://u:p@127.0.0.1:1/d?sslmode=disable&connect_timeout=1"}, 0)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if _, err := r.dbCount(ctx, r.adminURLs[0]); err == nil {
t.Error("dbCount on dead admin URL returned nil; want connect error")
}
}

func TestClusterRouter_DBCount_LiveCluster(t *testing.T) {
adminDSN := testAdminDSN()
if adminDSN == "" {
t.Skip("admin DSN unset")
}
r := newClusterRouter([]string{adminDSN}, 0)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
n, err := r.dbCount(ctx, adminDSN)
if err != nil {
t.Fatalf("dbCount: %v", err)
}
if n < 0 {
t.Errorf("dbCount = %d; want >= 0", n)
}
}

func TestClusterRouter_RefreshCounts_LiveAndDead(t *testing.T) {
adminDSN := testAdminDSN()
if adminDSN == "" {
t.Skip("admin DSN unset")
}
// One live cluster + one dead one — refreshCounts must succeed for the
// live one and preserve the dead one's previous count.
r := newClusterRouter([]string{adminDSN, "postgres://u:p@127.0.0.1:1/d?sslmode=disable&connect_timeout=1"}, 0)
// Seed the dead cluster's previous count so the keep-stale branch can be
// observed.
r.counts[1] = 42
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
r.refreshCounts(ctx)
if r.counts[1] != 42 {
t.Errorf("dead cluster count was overwritten = %d; want preserved 42", r.counts[1])
}
}

// TestClusterRouter_RefreshCounts_EmptyURLSkipped — an empty admin URL in the
// router slice is skipped without erroring (defensive branch).
func TestClusterRouter_RefreshCounts_EmptyURLSkipped(t *testing.T) {
r := newClusterRouter([]string{""}, 0)
r.refreshCounts(context.Background())
if r.counts[0] != 0 {
t.Errorf("empty URL produced non-zero count = %d; want 0", r.counts[0])
}
}

// TestClusterRouter_Pick_NoClusters covers Pick's "no clusters configured"
// branch — uncovered in baseline because every test that constructed a router
// passed at least one URL.
func TestClusterRouter_Pick_NoClusters(t *testing.T) {
r := &ClusterRouter{}
_, _, err := r.Pick()
if err == nil {
t.Error("Pick on empty router returned nil err; want 'no clusters configured'")
}
}

// TestClusterRouter_Pick_AllAtCapacity_StillReturnsBest covers the
// fall-back-to-index-0 branch when every cluster is at-or-above capacity.
func TestClusterRouter_Pick_AllAtCapacity_StillReturnsBest(t *testing.T) {
r := newClusterRouter([]string{"u0", "u1"}, 1)
// Saturate both clusters so headroom is 0 for both.
r.counts[0] = 1
r.counts[1] = 1
idx, url, err := r.Pick()
if err != nil {
t.Fatalf("Pick at capacity returned err = %v; want nil", err)
}
if idx != 0 || url != "u0" {
t.Errorf("Pick at capacity = (%d,%q); want (0,u0) fallback", idx, url)
}
}

// TestClusterRouter_Pick_AllEmptyURLs covers the "every URL is empty" branch
// where best stays -1 and the function falls through to index 0.
func TestClusterRouter_Pick_AllEmptyURLs(t *testing.T) {
r := &ClusterRouter{
adminURLs: []string{"", ""},
maxDBs: []int{400, 400},
counts: []int{0, 0},
inflight: []int{0, 0},
}
idx, _, err := r.Pick()
if err != nil {
t.Errorf("Pick(all-empty) err = %v; want nil fallback to index 0", err)
}
if idx != 0 {
t.Errorf("Pick(all-empty) = %d; want 0 fallback", idx)
}
}
14 changes: 7 additions & 7 deletions internal/backend/postgres/cluster_router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ func TestAdminURLForResource_RoutesByIndex(t *testing.T) {
prid string
want string
}{
{"", "url0"}, // legacy empty → cluster 0
{"local:0", "url0"}, // explicit index
{"local:2", "url2"}, // explicit index
{"local:9", "url0"}, // out of range → cluster 0
{"local:-1", "url0"}, // negative → cluster 0
{"local:x", "url0"}, // unparseable → cluster 0
{"neon:abc", "url0"}, // not a local resource → cluster 0
{"", "url0"}, // legacy empty → cluster 0
{"local:0", "url0"}, // explicit index
{"local:2", "url2"}, // explicit index
{"local:9", "url0"}, // out of range → cluster 0
{"local:-1", "url0"}, // negative → cluster 0
{"local:x", "url0"}, // unparseable → cluster 0
{"neon:abc", "url0"}, // not a local resource → cluster 0
}
for _, tc := range cases {
if got := r.AdminURLForResource(tc.prid); got != tc.want {
Expand Down
Loading
Loading