zlh-agent/internal/http/agent_test.go
2026-05-01 14:32:53 +00:00

244 lines
6.1 KiB
Go

package agenthttp
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gorilla/websocket"
"zlh-agent/internal/state"
)
func TestProtectedRoutesRequireAgentToken(t *testing.T) {
t.Setenv("ZLH_AGENT_TOKEN", "test-token")
protected := []struct {
method string
path string
body string
}{
{http.MethodGet, "/status", ""},
{http.MethodGet, "/ready", ""},
{http.MethodPost, "/config", "{}"},
{http.MethodPost, "/start", ""},
{http.MethodPost, "/stop", ""},
{http.MethodPost, "/restart", ""},
{http.MethodGet, "/game/files/list", ""},
{http.MethodGet, "/game/files/read", ""},
{http.MethodGet, "/game/files/download", ""},
{http.MethodPost, "/game/files/upload", ""},
{http.MethodPost, "/game/files/revert", ""},
{http.MethodGet, "/game/files/stat", ""},
{http.MethodGet, "/game/backups", ""},
{http.MethodPost, "/game/backups", ""},
{http.MethodPost, "/game/backups/restore", `{"id":"backup-1"}`},
{http.MethodDelete, "/game/backups/backup-1", ""},
{http.MethodGet, "/game/mods", ""},
{http.MethodPost, "/game/mods/install", "{}"},
{http.MethodPost, "/game/mods/mod-1", "{}"},
{http.MethodPost, "/agent/update", "{}"},
{http.MethodGet, "/agent/update/status", ""},
{http.MethodGet, "/metrics/process", ""},
{http.MethodPost, "/dev/codeserver/start", ""},
{http.MethodPost, "/dev/codeserver/stop", ""},
{http.MethodPost, "/dev/codeserver/restart", ""},
}
mux := NewMux()
for _, tc := range protected {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("without token status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
req = httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
req.Header.Set("Authorization", "Bearer test-token")
rec = httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code == http.StatusUnauthorized {
t.Fatalf("with token status = %d, want authorized request to reach handler", rec.Code)
}
})
}
}
func TestProtectedRoutesFailClosedWhenAgentTokenMissing(t *testing.T) {
t.Setenv("ZLH_AGENT_TOKEN", "")
mux := NewMux()
req := httptest.NewRequest(http.MethodGet, "/status", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
}
func TestPublicRoutesDoNotRequireAgentToken(t *testing.T) {
t.Setenv("ZLH_AGENT_TOKEN", "")
mux := NewMux()
for _, path := range []string{"/health", "/version"} {
t.Run(path, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code == http.StatusUnauthorized {
t.Fatalf("status = %d, want public route", rec.Code)
}
})
}
}
func TestConsoleWebSocketRequiresAgentToken(t *testing.T) {
t.Setenv("ZLH_AGENT_TOKEN", "test-token")
server := httptest.NewServer(NewMux())
defer server.Close()
conn, resp, err := websocket.DefaultDialer.Dial(strings.Replace(server.URL, "http://", "ws://", 1)+"/console/stream", nil)
if conn != nil {
_ = conn.Close()
}
if err == nil {
t.Fatalf("websocket dial without token succeeded")
}
if resp == nil {
t.Fatalf("websocket dial response is nil: %v", err)
}
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusUnauthorized)
}
}
func TestEnsureDevCodeServerInstallsAndStartsWhenRequested(t *testing.T) {
restoreCodeServerTestHooks(t)
cfg := &state.Config{
ContainerType: "dev",
Runtime: "node",
EnableCodeServer: true,
}
installed := false
running := false
installCalls := 0
startCalls := 0
verifyCalls := 0
codeServerInstalled = func() bool { return installed }
codeServerRunning = func() bool { return running }
codeServerInstall = func(state.Config) error {
installCalls++
installed = true
return nil
}
codeServerStart = func(state.Config) error {
startCalls++
running = true
return nil
}
codeServerVerify = func() error {
verifyCalls++
return nil
}
if err := ensureDevCodeServer(cfg); err != nil {
t.Fatalf("ensureDevCodeServer: %v", err)
}
if installCalls != 1 {
t.Fatalf("installCalls = %d, want 1", installCalls)
}
if startCalls != 1 {
t.Fatalf("startCalls = %d, want 1", startCalls)
}
if verifyCalls != 1 {
t.Fatalf("verifyCalls = %d, want 1", verifyCalls)
}
}
func TestEnsureDevCodeServerStartsInstalledStoppedAddon(t *testing.T) {
restoreCodeServerTestHooks(t)
cfg := &state.Config{
ContainerType: "dev",
Runtime: "node",
Addons: []string{"codeserver"},
}
running := false
installCalls := 0
startCalls := 0
codeServerInstalled = func() bool { return true }
codeServerRunning = func() bool { return running }
codeServerInstall = func(state.Config) error {
installCalls++
return nil
}
codeServerStart = func(state.Config) error {
startCalls++
running = true
return nil
}
codeServerVerify = func() error { return nil }
if err := ensureDevCodeServer(cfg); err != nil {
t.Fatalf("ensureDevCodeServer: %v", err)
}
if installCalls != 0 {
t.Fatalf("installCalls = %d, want 0", installCalls)
}
if startCalls != 1 {
t.Fatalf("startCalls = %d, want 1", startCalls)
}
}
func TestEnsureDevCodeServerSkipsWhenNotRequested(t *testing.T) {
restoreCodeServerTestHooks(t)
called := false
codeServerInstalled = func() bool {
called = true
return false
}
if err := ensureDevCodeServer(&state.Config{ContainerType: "dev", Runtime: "node"}); err != nil {
t.Fatalf("ensureDevCodeServer: %v", err)
}
if called {
t.Fatalf("code-server hooks were called for config without code-server request")
}
}
func restoreCodeServerTestHooks(t *testing.T) {
t.Helper()
oldInstall := codeServerInstall
oldStart := codeServerStart
oldVerify := codeServerVerify
oldInstalled := codeServerInstalled
oldRunning := codeServerRunning
t.Cleanup(func() {
codeServerInstall = oldInstall
codeServerStart = oldStart
codeServerVerify = oldVerify
codeServerInstalled = oldInstalled
codeServerRunning = oldRunning
})
}