serve_test.go
Rama main · réplica de lectura del repositorio original.
//go:build !desktop
package main
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"k8s.io/klog/v2"
"github.com/sophotechlabs/spinoza/internal/api"
"github.com/sophotechlabs/spinoza/internal/auth"
"github.com/sophotechlabs/spinoza/internal/server"
"github.com/sophotechlabs/spinoza/internal/store"
)
type wiredMode struct {
calls []string
tabs []store.Tab
err error
}
type idleExitRecorder struct {
quit func()
}
func useServerArgs(t *testing.T, args ...string) {
t.Helper()
original := os.Args
os.Args = append([]string{"spinoza"}, args...)
t.Cleanup(func() {
os.Args = original
})
}
func preserveServerLogger(t *testing.T) {
t.Helper()
previous := slog.Default()
t.Cleanup(func() {
slog.SetDefault(previous)
klog.SetSlogLogger(previous)
})
}
func useOwnConfigHome(t *testing.T) string {
t.Helper()
root := t.TempDir()
t.Setenv("HOME", root)
t.Setenv("XDG_CONFIG_HOME", root)
t.Setenv("AppData", root)
return root
}
func TestRunReportsAnInvalidLogLevel(t *testing.T) {
useOwnConfigHome(t)
useServerArgs(t, "-log-level", "verbose")
err := run()
if err == nil || !strings.Contains(err.Error(), "log level") {
t.Fatalf("error = %v, want the invalid log level", err)
}
}
func TestRunRefusesANonLoopbackLocalAddress(t *testing.T) {
preserveServerLogger(t)
useOwnConfigHome(t)
useServerArgs(t, "-addr", "0.0.0.0:34115")
err := run()
if err == nil || !strings.Contains(err.Error(), "not loopback") {
t.Fatalf("error = %v, want the loopback boundary", err)
}
}
func TestRunReportsAnExplicitContextThatCannotOpen(t *testing.T) {
preserveServerLogger(t)
config := filepath.Join(useOwnConfigHome(t), "missing-kubeconfig")
useServerArgs(t, "-kubeconfig", config, "-context", "missing")
err := run()
if err == nil || !strings.Contains(err.Error(), config) {
t.Fatalf("error = %v, want kubeconfig %q named", err, config)
}
}
func TestRunRemovesItsTokenAfterServerStartupFails(t *testing.T) {
preserveServerLogger(t)
configDir := useOwnConfigHome(t)
config := filepath.Join(configDir, "missing-kubeconfig")
token := filepath.Join(configDir, "access-token")
useServerArgs(
t,
"-addr", "127.0.0.1:not-a-port",
"-kubeconfig", config,
"-token-file", token,
)
err := run()
if err == nil || !strings.Contains(err.Error(), "server:") {
t.Fatalf("error = %v, want the listener startup failure", err)
}
if _, statErr := os.Stat(token); !os.IsNotExist(statErr) {
t.Fatalf("token file error = %v, want it removed", statErr)
}
}
func (r *idleExitRecorder) UseIdleExit(quit func()) {
r.quit = quit
}
func (w *wiredMode) UseClusterAuth(server.ClusterAuth) {
w.calls = append(w.calls, "authentication")
}
func (w *wiredMode) RestoreTabs(ctx context.Context, held server.Tabs) {
w.calls = append(w.calls, "timeline")
w.tabs, w.err = held.All(ctx)
}
func (w *wiredMode) UseUpdates(server.Updates) {
w.calls = append(w.calls, "updates")
}
func (w *wiredMode) UseInstaller(server.Installs) {
w.calls = append(w.calls, "installer")
}
func storedTimeline(t *testing.T) *store.Store {
t.Helper()
past, err := store.Open(t.Context(), filepath.Join(t.TempDir(), "history.db"))
if err != nil {
t.Fatalf("open history: %v", err)
}
t.Cleanup(func() { _ = past.Close() })
tabs := past.Tabs()
rememberErr := tabs.Remember(t.Context(), store.Tab{ID: "cluster-1", Context: "in-cluster"})
if rememberErr != nil {
t.Fatalf("remember cluster: %v", rememberErr)
}
recordingErr := tabs.Recording(t.Context(), "cluster-1", "workloads")
if recordingErr != nil {
t.Fatalf("remember timeline: %v", recordingErr)
}
return past
}
func TestClusterModeRestoresItsPersistedTimeline(t *testing.T) {
past := storedTimeline(t)
wired := &wiredMode{}
err := wireMode(t.Context(), wired, servedOpts(nil), past)
if err != nil {
t.Fatalf("wire cluster mode: %v", err)
}
if wired.err != nil {
t.Fatalf("read restored tabs: %v", wired.err)
}
if strings.Join(wired.calls, ",") != "authentication,timeline" {
t.Fatalf("wiring order = %v, want authentication before the timeline", wired.calls)
}
if len(wired.tabs) != 1 || wired.tabs[0].Timeline != "workloads" {
t.Fatalf("restored tabs = %+v, want the persisted timeline", wired.tabs)
}
}
func TestClusterModeDoesNotRestoreTimelineStateWhenAuthenticationCannotStart(t *testing.T) {
wired := &wiredMode{}
opts := servedOpts(func(opts *settings) {
opts.serve.auth.Mode = "ldap"
})
err := wireMode(t.Context(), wired, opts, storedTimeline(t))
if err == nil {
t.Fatal("cluster mode restored state after authentication failed")
}
if len(wired.calls) != 0 {
t.Fatalf("wiring continued with calls %v after authentication failed", wired.calls)
}
}
func TestLocalModeStillRestoresTabsBeforeItsUpdateServices(t *testing.T) {
wired := &wiredMode{}
err := wireMode(t.Context(), wired, settings{}, storedTimeline(t))
if err != nil {
t.Fatalf("wire local mode: %v", err)
}
if strings.Join(wired.calls, ",") != "timeline,updates,installer" {
t.Fatalf("wiring order = %v, want the timeline before local services", wired.calls)
}
}
func TestLocalListeningStopsAfterTheLastViewLeaves(t *testing.T) {
recorded := &idleExitRecorder{}
idle := make(chan struct{})
announceListening(t.Context(), recorded, settings{addr: "127.0.0.1:34115"}, "secret", idle)
if recorded.quit == nil {
t.Fatal("local listening did not register an idle exit")
}
recorded.quit()
recorded.quit()
select {
case <-idle:
default:
t.Fatal("the last view leaving did not stop local listening")
}
}
func TestClusterListeningDoesNotStopWhenThereAreNoViews(t *testing.T) {
recorded := &idleExitRecorder{}
idle := make(chan struct{})
announceListening(t.Context(), recorded, servedOpts(nil), "", idle)
if recorded.quit != nil {
t.Fatal("cluster listening registered a local idle exit")
}
select {
case <-idle:
t.Fatal("cluster listening stopped without a view")
default:
}
}
func servedOpts(change func(*settings)) settings {
opts := settings{}
opts.serve = serving{
on: true,
publicURL: "https://spinoza.example.com",
impersonate: true,
auth: auth.Config{
Mode: auth.ModeProxy,
PublicURL: "https://spinoza.example.com",
Proxy: auth.ProxyConfig{SharedSecret: auth.NewSecret()},
},
}
if change != nil {
change(&opts)
}
return opts
}
func TestServingTurnsTheServerIntoOneThatAsksWhoYouAre(t *testing.T) {
srv := server.New(nil, nil, "")
err := serveTeam(t.Context(), srv, servedOpts(nil), nil)
if err != nil {
t.Fatalf("setting up cluster mode: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/auth/me", http.NoBody)
req.Host = "spinoza.example.com"
recorded := httptest.NewRecorder()
srv.Handler().ServeHTTP(recorded, req)
var found api.Session
if decodeErr := json.Unmarshal(recorded.Body.Bytes(), &found); decodeErr != nil {
t.Fatalf("decoding the session: %v", decodeErr)
}
if !found.Cluster || found.Mode != auth.ModeProxy {
t.Fatalf("session = %+v, want it serving a cluster behind a proxy", found)
}
if found.Authenticated {
t.Fatal("a request with no identity on it read as signed in")
}
}
func TestServingWithExplicitAnonymousAdminAccessStarts(t *testing.T) {
opts := servedOpts(func(opts *settings) {
opts.serve.auth.Mode = auth.ModeNone
opts.serve.auth.AllowAnonymous = true
opts.serve.impersonate = false
})
if err := serveTeam(t.Context(), server.New(nil, nil, ""), opts, nil); err != nil {
t.Fatalf("setting up cluster mode: %v", err)
}
}
func TestAnAuthConfigThatCannotWorkStopsTheServerStarting(t *testing.T) {
opts := servedOpts(func(opts *settings) {
opts.serve.auth.Mode = "ldap"
})
err := serveTeam(t.Context(), server.New(nil, nil, ""), opts, nil)
if err == nil {
t.Fatal("cluster mode started with an auth mode nobody implements")
}
if !strings.Contains(err.Error(), "ldap") {
t.Fatalf("error = %q, want it to name the mode", err.Error())
}
}
func TestOnlyALocalRunHasToBindLoopback(t *testing.T) {
local := settings{addr: "0.0.0.0:8080"}
if err := checkListen(local); err == nil {
t.Fatal("a local run was allowed to bind every interface")
}
if err := checkListen(servedOpts(func(opts *settings) { opts.addr = "0.0.0.0:8080" })); err != nil {
t.Fatalf("a served run was refused its own address: %v", err)
}
}
func TestOnlyALocalRunMintsAToken(t *testing.T) {
served, err := runToken(servedOpts(nil))
if err != nil {
t.Fatalf("minting: %v", err)
}
if served != "" {
t.Fatal("a served spinoza minted a run token nobody would ever be given")
}
path := filepath.Join(t.TempDir(), "token")
token, localErr := runToken(settings{tokenFile: path})
if localErr != nil {
t.Fatalf("minting: %v", localErr)
}
if token == "" {
t.Fatal("a local run minted no token")
}
body, readErr := os.ReadFile(path)
if readErr != nil {
t.Fatalf("reading the token file: %v", readErr)
}
if strings.TrimSpace(string(body)) != token {
t.Fatalf("token file holds %q, want %q", strings.TrimSpace(string(body)), token)
}
}
func TestATokenFileThatCannotBeWrittenStopsTheRun(t *testing.T) {
_, err := runToken(settings{tokenFile: filepath.Join(t.TempDir(), "no", "such", "dir", "token")})
if err == nil {
t.Fatal("a token file spinoza could not write was ignored")
}
}
func TestWhoHelmAndKubectlActAs(t *testing.T) {
inCluster := func() bool { return true }
elsewhere := func() bool { return false }
dir := func() (string, error) { return "/state", nil }
noDir := func() (string, error) { return "", errors.New("no config directory") }
wrote := func(string) (string, error) { return "/state/spinoza/in-cluster.kubeconfig", nil }
broke := func(string) (string, error) { return "", errors.New("read-only") }
cases := []struct {
name string
opts settings
in func() bool
dir func() (string, error)
with func(string) (string, error)
want string
}{
{
name: "a local run needs none",
opts: settings{},
in: inCluster,
dir: dir,
with: wrote,
want: "",
},
{
name: "a served run outside a cluster reads the kubeconfig it was given",
opts: servedOpts(nil),
in: elsewhere,
dir: dir,
with: wrote,
want: "",
},
{
name: "a kubeconfig of your own wins",
opts: servedOpts(func(opts *settings) { opts.cluster.Kubeconfig = "/etc/kube.yaml" }),
in: inCluster,
dir: dir,
with: wrote,
want: "",
},
{
name: "nowhere to write one",
opts: servedOpts(nil),
in: inCluster,
dir: noDir,
with: wrote,
want: "",
},
{
name: "writing one failed",
opts: servedOpts(nil),
in: inCluster,
dir: dir,
with: broke,
want: "",
},
{
name: "in a pod, with somewhere to write it",
opts: servedOpts(nil),
in: inCluster,
dir: dir,
with: wrote,
want: "/state/spinoza/in-cluster.kubeconfig",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := toolKubeconfigFrom(tc.opts, tc.in, tc.dir, tc.with); got != tc.want {
t.Fatalf("kubeconfig = %q, want %q", got, tc.want)
}
})
}
}
func TestAServerNotServingAClusterWritesNoKubeconfig(t *testing.T) {
if got := toolKubeconfig(settings{}); got != "" {
t.Fatalf("kubeconfig = %q, want none when spinoza runs as your own window", got)
}
}