f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
382 lines
9.6 KiB
Go
382 lines
9.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.kenn.io/agentsview/internal/config"
|
|
)
|
|
|
|
func TestBrowserURLUsesPublicURL(t *testing.T) {
|
|
cfg := config.Config{
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
PublicURL: "https://viewer.example.test",
|
|
}
|
|
assert.Equal(t, "https://viewer.example.test", browserURL(cfg))
|
|
}
|
|
|
|
func TestBrowserURLWithPlatformUsesWSLEth0ForBindAll(t *testing.T) {
|
|
cfg := config.Config{Host: "0.0.0.0", Port: 8080}
|
|
|
|
got := browserURLWithPlatform(
|
|
cfg,
|
|
func() bool { return true },
|
|
func(name string) (string, bool) {
|
|
assert.Equal(t, "eth0", name)
|
|
return "172.20.10.5", true
|
|
},
|
|
)
|
|
|
|
assert.Equal(t, "http://172.20.10.5:8080", got)
|
|
}
|
|
|
|
func TestBrowserURLWithPlatformKeepsLoopbackOutsideWSL(t *testing.T) {
|
|
cfg := config.Config{Host: "0.0.0.0", Port: 8080}
|
|
|
|
got := browserURLWithPlatform(
|
|
cfg,
|
|
func() bool { return false },
|
|
func(string) (string, bool) {
|
|
t.Fatal("interface lookup should not run outside WSL")
|
|
return "", false
|
|
},
|
|
)
|
|
|
|
assert.Equal(t, "http://127.0.0.1:8080", got)
|
|
}
|
|
|
|
func TestBrowserURLWithPlatformKeepsLoopbackWhenWSLEth0Missing(t *testing.T) {
|
|
cfg := config.Config{Host: "0.0.0.0", Port: 8080}
|
|
|
|
got := browserURLWithPlatform(
|
|
cfg,
|
|
func() bool { return true },
|
|
func(string) (string, bool) { return "", false },
|
|
)
|
|
|
|
assert.Equal(t, "http://127.0.0.1:8080", got)
|
|
}
|
|
|
|
func TestValidateServeConfigManagedCaddyAllowsHTTPS(t *testing.T) {
|
|
dir := t.TempDir()
|
|
certPath := filepath.Join(dir, "viewer.crt")
|
|
keyPath := filepath.Join(dir, "viewer.key")
|
|
require.NoError(t, os.WriteFile(certPath, []byte("cert"), 0o600))
|
|
require.NoError(t, os.WriteFile(keyPath, []byte("key"), 0o600))
|
|
|
|
cfg := config.Config{
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
PublicURL: "https://viewer.example.test",
|
|
Proxy: config.ProxyConfig{
|
|
Mode: "caddy",
|
|
Bin: os.Args[0],
|
|
TLSCert: certPath,
|
|
TLSKey: keyPath,
|
|
AllowedSubnets: []string{"10.0.0.0/16"},
|
|
},
|
|
}
|
|
assert.NoError(t, validateServeConfig(cfg))
|
|
}
|
|
|
|
func TestValidateServeConfigManagedCaddyRejectsNonLoopbackHost(t *testing.T) {
|
|
cfg := config.Config{
|
|
Host: "0.0.0.0",
|
|
Port: 8080,
|
|
PublicURL: "http://viewer.example.test:8004",
|
|
RequireAuth: true,
|
|
Proxy: config.ProxyConfig{
|
|
Mode: "caddy",
|
|
Bin: os.Args[0],
|
|
},
|
|
}
|
|
err := validateServeConfig(cfg)
|
|
require.Error(t, err, "expected error for non-loopback backend host")
|
|
assert.Contains(t, err.Error(), "loopback backend host")
|
|
}
|
|
|
|
func TestValidateServeConfigNonLoopbackHostGuardrail(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg config.Config
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "config host without auth is rejected",
|
|
cfg: config.Config{
|
|
Host: "0.0.0.0", Port: 8080,
|
|
},
|
|
wantErr: "require_auth",
|
|
},
|
|
{
|
|
name: "config host with require_auth is allowed",
|
|
cfg: config.Config{
|
|
Host: "0.0.0.0", Port: 8080,
|
|
RequireAuth: true,
|
|
},
|
|
},
|
|
{
|
|
name: "explicit --host flag stays allowed without auth",
|
|
cfg: config.Config{
|
|
Host: "0.0.0.0", Port: 8080,
|
|
HostExplicit: true,
|
|
},
|
|
},
|
|
{
|
|
name: "loopback host needs no auth",
|
|
cfg: config.Config{
|
|
Host: "127.0.0.1", Port: 8080,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateServeConfig(tt.cfg)
|
|
if tt.wantErr == "" {
|
|
assert.NoError(t, err)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateServeConfigManagedCaddyRequiresAllowlistForNonLoopbackBind(t *testing.T) {
|
|
dir := t.TempDir()
|
|
certPath := filepath.Join(dir, "viewer.crt")
|
|
keyPath := filepath.Join(dir, "viewer.key")
|
|
require.NoError(t, os.WriteFile(certPath, []byte("cert"), 0o600))
|
|
require.NoError(t, os.WriteFile(keyPath, []byte("key"), 0o600))
|
|
|
|
cfg := config.Config{
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
PublicURL: "https://viewer.example.test:8443",
|
|
Proxy: config.ProxyConfig{
|
|
Mode: "caddy",
|
|
Bin: os.Args[0],
|
|
BindHost: "0.0.0.0",
|
|
TLSCert: certPath,
|
|
TLSKey: keyPath,
|
|
},
|
|
}
|
|
err := validateServeConfig(cfg)
|
|
require.Error(t, err, "expected non-loopback bind allowlist error")
|
|
assert.Contains(t, err.Error(), "allowed_subnet")
|
|
}
|
|
|
|
func TestValidateServeConfigManagedCaddyRejectsHTTPWithTLS(t *testing.T) {
|
|
cfg := config.Config{
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
PublicURL: "http://viewer.example.test:8004",
|
|
Proxy: config.ProxyConfig{
|
|
Mode: "caddy",
|
|
Bin: os.Args[0],
|
|
TLSCert: "/tmp/viewer.crt",
|
|
TLSKey: "/tmp/viewer.key",
|
|
},
|
|
}
|
|
err := validateServeConfig(cfg)
|
|
require.Error(t, err, "expected HTTP-with-TLS error")
|
|
assert.Contains(t, err.Error(), "HTTP mode")
|
|
}
|
|
|
|
func TestBuildManagedCaddyfileIncludesAllowlistAndTLS(t *testing.T) {
|
|
got := buildManagedCaddyfile(
|
|
"https://viewer.example.test:8443",
|
|
"0.0.0.0",
|
|
"127.0.0.1:8080",
|
|
"/tmp/viewer.crt",
|
|
"/tmp/viewer.key",
|
|
[]string{"10.0.0.0/16", "192.168.1.0/24"},
|
|
)
|
|
|
|
for _, want := range []string{
|
|
"admin off",
|
|
"auto_https off",
|
|
"https://viewer.example.test:8443 {",
|
|
"bind 0.0.0.0",
|
|
"@blocked not remote_ip 10.0.0.0/16 192.168.1.0/24",
|
|
"respond @blocked \"Forbidden\" 403",
|
|
"tls \"/tmp/viewer.crt\" \"/tmp/viewer.key\"",
|
|
"reverse_proxy 127.0.0.1:8080",
|
|
} {
|
|
assert.Contains(t, got, want,
|
|
"generated caddyfile missing %q", want)
|
|
}
|
|
}
|
|
|
|
func TestManagedCaddyConfigPathNamespacesMode(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
|
|
gotServe := managedCaddyConfigPath(dataDir, "serve")
|
|
gotPG := managedCaddyConfigPath(dataDir, "pg-serve")
|
|
|
|
assert.NotEqual(t, gotServe, gotPG,
|
|
"managed caddy paths must differ by mode")
|
|
assert.True(t, strings.HasSuffix(
|
|
gotServe,
|
|
filepath.Join("managed-caddy", "serve", "Caddyfile"),
|
|
), "serve path = %q", gotServe)
|
|
assert.True(t, strings.HasSuffix(
|
|
gotPG,
|
|
filepath.Join("managed-caddy", "pg-serve", "Caddyfile"),
|
|
), "pg path = %q", gotPG)
|
|
}
|
|
|
|
func TestPrepareManagedCaddyConfigForPGServeUsesNamespacedPathAndBackend(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
cfg := config.Config{
|
|
DataDir: dataDir,
|
|
PublicURL: "https://viewer.example.test",
|
|
Proxy: config.ProxyConfig{
|
|
BindHost: "0.0.0.0",
|
|
TLSCert: "/tmp/viewer.crt",
|
|
TLSKey: "/tmp/viewer.key",
|
|
AllowedSubnets: []string{"10.0.0.0/16"},
|
|
},
|
|
}
|
|
|
|
path, content, err := prepareManagedCaddyConfig(
|
|
cfg,
|
|
"pg-serve",
|
|
"127.0.0.1:18080",
|
|
)
|
|
require.NoError(t, err)
|
|
assert.True(t, strings.HasSuffix(
|
|
path,
|
|
filepath.Join("managed-caddy", "pg-serve", "Caddyfile"),
|
|
), "path = %q", path)
|
|
assert.Contains(t, content, "reverse_proxy 127.0.0.1:18080")
|
|
}
|
|
|
|
func TestRewriteConfiguredPublicURLPort_RewritesMatchingExplicitPort(t *testing.T) {
|
|
updatedURL, updatedOrigins, changed, err := rewriteConfiguredPublicURLPort(
|
|
"http://viewer.example.test:8004",
|
|
[]string{"http://viewer.example.test:8004"},
|
|
8004,
|
|
8005,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.True(t, changed, "expected public URL rewrite")
|
|
assert.Equal(t, "http://viewer.example.test:8005", updatedURL)
|
|
assert.Equal(t, "http://viewer.example.test:8005",
|
|
strings.Join(updatedOrigins, ","))
|
|
}
|
|
|
|
func TestRewriteConfiguredPublicURLPort_PreservesExternalProxyPort(t *testing.T) {
|
|
updatedURL, updatedOrigins, changed, err := rewriteConfiguredPublicURLPort(
|
|
"https://viewer.example.test",
|
|
[]string{"https://viewer.example.test"},
|
|
8080,
|
|
8081,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.False(t, changed, "expected public URL to remain unchanged")
|
|
assert.Equal(t, "https://viewer.example.test", updatedURL)
|
|
assert.Equal(t, "https://viewer.example.test",
|
|
strings.Join(updatedOrigins, ","))
|
|
}
|
|
|
|
func TestReadinessProbeHost(t *testing.T) {
|
|
tests := map[string]string{
|
|
"": "127.0.0.1",
|
|
"0.0.0.0": "127.0.0.1",
|
|
"::": "::1",
|
|
"127.0.0.1": "127.0.0.1",
|
|
"10.0.60.2": "10.0.60.2",
|
|
}
|
|
for input, want := range tests {
|
|
assert.Equal(t, want, readinessProbeHost(input),
|
|
"readinessProbeHost(%q)", input)
|
|
}
|
|
}
|
|
|
|
func TestWaitForLocalPortReturnsEarlyOnErrorChannel(t *testing.T) {
|
|
errCh := make(chan error, 1)
|
|
errCh <- errors.New("backend failed")
|
|
err := waitForLocalPort(
|
|
context.Background(),
|
|
"127.0.0.1",
|
|
65535,
|
|
5*time.Second,
|
|
errCh,
|
|
)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "backend failed")
|
|
}
|
|
|
|
func TestWaitForLocalPortHonorsContextCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
err := waitForLocalPort(
|
|
ctx,
|
|
"127.0.0.1",
|
|
65535,
|
|
5*time.Second,
|
|
nil,
|
|
)
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
func TestWaitForLocalPortPrefersContextCancellationOverError(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
errCh := make(chan error, 1)
|
|
errCh <- errors.New("caddy exited")
|
|
err := waitForLocalPort(
|
|
ctx,
|
|
"127.0.0.1",
|
|
65535,
|
|
5*time.Second,
|
|
errCh,
|
|
)
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
type countingCaddyGuard struct{ closed int }
|
|
|
|
func (g *countingCaddyGuard) Close() error {
|
|
g.closed++
|
|
return nil
|
|
}
|
|
|
|
func TestManagedCaddyStopClosesGuard(t *testing.T) {
|
|
guard := &countingCaddyGuard{}
|
|
called := 0
|
|
m := &managedCaddy{
|
|
cancel: func() { called++ },
|
|
guard: guard,
|
|
}
|
|
m.Stop()
|
|
assert.Equal(t, 1, called, "Stop must cancel the run context")
|
|
assert.Equal(t, 1, guard.closed, "Stop must close the lifetime guard")
|
|
}
|
|
|
|
func TestManagedCaddyStopNilSafe(t *testing.T) {
|
|
var m *managedCaddy
|
|
assert.NotPanics(t, func() { m.Stop() })
|
|
assert.NotPanics(t, func() { (&managedCaddy{}).Stop() })
|
|
}
|
|
|
|
func TestNewCaddyGuardNoopOnPosix(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("POSIX no-op guard; Windows builds a job-object guard")
|
|
}
|
|
guard, err := newCaddyGuard(nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, guard)
|
|
require.NoError(t, guard.Close())
|
|
}
|