mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 00:16:38 +00:00
306 lines
10 KiB
Go
306 lines
10 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type fakeGatewayUpgradeRunner struct {
|
|
tags []string
|
|
err error
|
|
}
|
|
|
|
func (r *fakeGatewayUpgradeRunner) Start(tag string) error {
|
|
r.tags = append(r.tags, tag)
|
|
return r.err
|
|
}
|
|
|
|
func TestValidGatewayUpgradeTag(t *testing.T) {
|
|
tests := []struct {
|
|
tag string
|
|
want bool
|
|
}{
|
|
{"latest", true},
|
|
{"v3.12.0", true},
|
|
{"v3.12.0-beta.1", true},
|
|
{"v3.12.0-rc.2", true},
|
|
{"", false},
|
|
{"3.12.0", false},
|
|
{"v3.12", false},
|
|
{"v3.12.0-beta", false},
|
|
{"https://example.com/goclaw.tar.gz", false},
|
|
{"v3.12.0;reboot", false},
|
|
{"../v3.12.0", false},
|
|
{"v3.12.0 linux", false},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := validGatewayUpgradeTag(tt.tag); got != tt.want {
|
|
t.Fatalf("validGatewayUpgradeTag(%q) = %v, want %v", tt.tag, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeStatusMissingReturnsIdle(t *testing.T) {
|
|
h := &GatewayUpgradeHandler{StatusPath: filepath.Join(t.TempDir(), "missing.json"), TriggerToken: "secret-token"}
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/system/gateway/upgrade/status", nil)
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-status-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStatus(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("want 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if body["state"] != "idle" {
|
|
t.Fatalf("state = %v, want idle", body["state"])
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeStartAcceptsValidTag(t *testing.T) {
|
|
runner := &fakeGatewayUpgradeRunner{}
|
|
h := &GatewayUpgradeHandler{
|
|
StatusPath: filepath.Join(t.TempDir(), "status.json"),
|
|
TriggerToken: "secret-token",
|
|
Runner: runner,
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"v3.12.0"}`))
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-start-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStart(w, req)
|
|
|
|
if w.Code != http.StatusAccepted {
|
|
t.Fatalf("want 202, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if len(runner.tags) != 1 || runner.tags[0] != "v3.12.0" {
|
|
t.Fatalf("runner tags = %#v, want [v3.12.0]", runner.tags)
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeStartRejectsInvalidTag(t *testing.T) {
|
|
runner := &fakeGatewayUpgradeRunner{}
|
|
h := &GatewayUpgradeHandler{
|
|
StatusPath: filepath.Join(t.TempDir(), "status.json"),
|
|
TriggerToken: "secret-token",
|
|
Runner: runner,
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"https://example.com/x"}`))
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-invalid-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStart(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("want 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if len(runner.tags) != 0 {
|
|
t.Fatalf("runner should not be called, got %#v", runner.tags)
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeStartRejectsRunningJob(t *testing.T) {
|
|
dir := t.TempDir()
|
|
statusPath := filepath.Join(dir, "status.json")
|
|
if err := os.WriteFile(statusPath, []byte(`{"state":"running"}`), 0o600); err != nil {
|
|
t.Fatalf("write status: %v", err)
|
|
}
|
|
runner := &fakeGatewayUpgradeRunner{}
|
|
h := &GatewayUpgradeHandler{StatusPath: statusPath, TriggerToken: "secret-token", Runner: runner}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"latest"}`))
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-running-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStart(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("want 409, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if len(runner.tags) != 0 {
|
|
t.Fatalf("runner should not be called, got %#v", runner.tags)
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeStartAllowsStaleRunningJob(t *testing.T) {
|
|
dir := t.TempDir()
|
|
statusPath := filepath.Join(dir, "status.json")
|
|
staleStartedAt := time.Now().UTC().Add(-gatewayUpgradeRunningMaxAge - time.Minute).Format(time.RFC3339)
|
|
if err := os.WriteFile(statusPath, []byte(`{"state":"running","startedAt":"`+staleStartedAt+`"}`), 0o600); err != nil {
|
|
t.Fatalf("write status: %v", err)
|
|
}
|
|
runner := &fakeGatewayUpgradeRunner{}
|
|
h := &GatewayUpgradeHandler{StatusPath: statusPath, TriggerToken: "secret-token", Runner: runner}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"latest"}`))
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-stale-running-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStart(w, req)
|
|
|
|
if w.Code != http.StatusAccepted {
|
|
t.Fatalf("want 202, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if len(runner.tags) != 1 || runner.tags[0] != "latest" {
|
|
t.Fatalf("runner tags = %#v, want [latest]", runner.tags)
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeTriggerTokenGuard(t *testing.T) {
|
|
runner := &fakeGatewayUpgradeRunner{}
|
|
h := &GatewayUpgradeHandler{
|
|
StatusPath: filepath.Join(t.TempDir(), "status.json"),
|
|
TriggerToken: "secret-token",
|
|
Runner: runner,
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"latest"}`))
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-token-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStart(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("want 403, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if len(runner.tags) != 0 {
|
|
t.Fatalf("runner should not be called, got %#v", runner.tags)
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeRejectsNonMasterScope(t *testing.T) {
|
|
runner := &fakeGatewayUpgradeRunner{}
|
|
h := &GatewayUpgradeHandler{
|
|
StatusPath: filepath.Join(t.TempDir(), "status.json"),
|
|
TriggerToken: "secret-token",
|
|
Runner: runner,
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"latest"}`))
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
req = req.WithContext(tenantAdminCtx(req.Context(), "gateway-tenant-admin"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStart(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("want 403, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if len(runner.tags) != 0 {
|
|
t.Fatalf("runner should not be called, got %#v", runner.tags)
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeStartRunnerError(t *testing.T) {
|
|
h := &GatewayUpgradeHandler{
|
|
StatusPath: filepath.Join(t.TempDir(), "status.json"),
|
|
TriggerToken: "secret-token",
|
|
Runner: &fakeGatewayUpgradeRunner{err: errors.New("boom")},
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"latest"}`))
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-runner-error-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStart(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("want 500, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeFailsClosedWithoutConfiguredTriggerToken(t *testing.T) {
|
|
runner := &fakeGatewayUpgradeRunner{}
|
|
h := &GatewayUpgradeHandler{
|
|
StatusPath: filepath.Join(t.TempDir(), "status.json"),
|
|
Runner: runner,
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"latest"}`))
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-no-token-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStart(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("want 503, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if len(runner.tags) != 0 {
|
|
t.Fatalf("runner should not be called, got %#v", runner.tags)
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeRegisterRoutes(t *testing.T) {
|
|
runner := &fakeGatewayUpgradeRunner{}
|
|
h := &GatewayUpgradeHandler{
|
|
StatusPath: filepath.Join(t.TempDir(), "status.json"),
|
|
TriggerToken: "secret-token",
|
|
Runner: runner,
|
|
}
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/system/gateway/upgrade", bytes.NewBufferString(`{"tag":"latest"}`))
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
w := httptest.NewRecorder()
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusAccepted {
|
|
t.Fatalf("want 202, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if len(runner.tags) != 1 || runner.tags[0] != "latest" {
|
|
t.Fatalf("runner tags = %#v, want [latest]", runner.tags)
|
|
}
|
|
}
|
|
|
|
func TestNewGatewayUpgradeHandlerFromEnvDefaults(t *testing.T) {
|
|
t.Setenv("GOCLAW_UPGRADE_SCRIPT", "")
|
|
t.Setenv("GOCLAW_UPGRADE_STATUS_PATH", "")
|
|
t.Setenv("GOCLAW_UPGRADE_TRIGGER_TOKEN", "secret-token")
|
|
|
|
h := NewGatewayUpgradeHandlerFromEnv()
|
|
|
|
if h.ScriptPath != defaultGatewayUpgradeScript {
|
|
t.Fatalf("ScriptPath = %q, want %q", h.ScriptPath, defaultGatewayUpgradeScript)
|
|
}
|
|
if h.StatusPath != defaultGatewayUpgradeStatus {
|
|
t.Fatalf("StatusPath = %q, want %q", h.StatusPath, defaultGatewayUpgradeStatus)
|
|
}
|
|
if h.TriggerToken != "secret-token" {
|
|
t.Fatalf("TriggerToken was not loaded from env")
|
|
}
|
|
}
|
|
|
|
func TestGatewayUpgradeStatusRejectsInvalidJSON(t *testing.T) {
|
|
statusPath := filepath.Join(t.TempDir(), "status.json")
|
|
if err := os.WriteFile(statusPath, []byte(`{bad json`), 0o600); err != nil {
|
|
t.Fatalf("write status: %v", err)
|
|
}
|
|
h := &GatewayUpgradeHandler{StatusPath: statusPath, TriggerToken: "secret-token"}
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/system/gateway/upgrade/status", nil)
|
|
req.Header.Set(gatewayUpgradeTokenHeader, "secret-token")
|
|
req = req.WithContext(ownerCtx(req.Context(), "gateway-invalid-status-owner"))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleStatus(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("want 500, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if strings.Contains(w.Body.String(), statusPath) || strings.Contains(w.Body.String(), "decode upgrade status") {
|
|
t.Fatalf("response leaked internal status details: %s", w.Body.String())
|
|
}
|
|
}
|