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 }) }