Files
ServerStatus/server/http_server_test.go
T
2026-07-10 15:35:10 +08:00

159 lines
8.0 KiB
Go

package main
import (
"encoding/json"
"net/http"
"net/url"
"os"
"strings"
"testing"
)
func TestHTTPAPIAndStaticUI(t *testing.T) {
app := newTestApp(t, minimalTestConfig())
router := app.router()
response := performRequest(router, http.MethodGet, "/", "", "")
if response.Code != 200 || !strings.Contains(response.Body.String(), "test-ui") {
t.Fatalf("static UI: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodGet, "/api/health", "", "")
if response.Code != 200 || !strings.Contains(response.Body.String(), `"enabled":true`) {
t.Fatalf("health: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodGet, "/api/openapi.json", "", "")
if response.Code != 200 {
t.Fatalf("openapi: status=%d body=%s", response.Code, response.Body.String())
}
var openapi map[string]any
if err := json.Unmarshal(response.Body.Bytes(), &openapi); err != nil {
t.Fatal(err)
}
paths := openapi["paths"].(map[string]any)
if openapi["openapi"] != "3.1.0" || paths["/api/servers/{username}"] == nil || paths["/api/watchdog/{id}"] == nil {
t.Fatalf("OpenAPI document is incomplete: %#v", openapi)
}
monitorOperations := paths["/api/monitors"].(map[string]any)
createResponses := monitorOperations["post"].(map[string]any)["responses"].(map[string]any)
if createResponses["201"] == nil || createResponses["200"] != nil {
t.Fatalf("OpenAPI create response must describe HTTP 201: %#v", createResponses)
}
listResponses := monitorOperations["get"].(map[string]any)["responses"].(map[string]any)
listSchema := listResponses["200"].(map[string]any)["content"].(map[string]any)["application/json"].(map[string]any)["schema"].(map[string]any)
if listSchema["properties"].(map[string]any)["monitors"] == nil {
t.Fatalf("OpenAPI list response does not expose monitors: %#v", listSchema)
}
restartResponses := paths["/api/restart"].(map[string]any)["post"].(map[string]any)["responses"].(map[string]any)
if restartResponses["202"] == nil {
t.Fatalf("OpenAPI restart response must describe HTTP 202: %#v", restartResponses)
}
response = performRequest(router, http.MethodGet, "/api/config", "", "wrong")
if response.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", response.Code)
}
response = performRequest(router, http.MethodGet, "/api/config", "", "test-token")
if response.Code != 200 {
t.Fatalf("config: status=%d body=%s", response.Code, response.Body.String())
}
monitor := `{"name":"新增服务","host":"tcp://127.0.0.1:80","type":"tcp","interval":"30"}`
response = performRequest(router, http.MethodPost, "/api/monitors", monitor, "test-token")
if response.Code != http.StatusCreated || !strings.Contains(response.Body.String(), "新增服务") {
t.Fatalf("create monitor: status=%d body=%s", response.Code, response.Body.String())
}
path := "/api/monitors/" + url.PathEscape("新增服务")
monitor = `{"name":"更新服务","host":"https://example.org","type":"https","interval":45}`
response = performRequest(router, http.MethodPut, path, monitor, "test-token")
if response.Code != 200 || !strings.Contains(response.Body.String(), "更新服务") {
t.Fatalf("update monitor: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodDelete, "/api/monitors/"+url.PathEscape("更新服务"), "", "test-token")
if response.Code != 200 {
t.Fatalf("delete monitor: status=%d body=%s", response.Code, response.Body.String())
}
percentMonitor := `{"name":"rate%check","host":"https://example.com","type":"https","interval":30}`
response = performRequest(router, http.MethodPost, "/api/monitors", percentMonitor, "test-token")
if response.Code != http.StatusCreated {
t.Fatalf("create percent monitor: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodDelete, "/api/monitors/"+url.PathEscape("rate%check"), "", "test-token")
if response.Code != 200 {
t.Fatalf("delete percent monitor: status=%d body=%s", response.Code, response.Body.String())
}
server := `{"username":"s02","name":"node2","type":"kvm","host":"host2","location":"JP","password":"secret","monthstart":31}`
response = performRequest(router, http.MethodPost, "/api/servers", server, "test-token")
if response.Code != http.StatusCreated || !strings.Contains(response.Body.String(), `"monthstart":28`) {
t.Fatalf("create server: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodDelete, "/api/servers/s02", "", "test-token")
if response.Code != 200 {
t.Fatalf("delete server: status=%d body=%s", response.Code, response.Body.String())
}
numericServer := `{"username":"0","name":"numeric","type":"kvm","host":"host0","location":"US","password":"secret","monthstart":1}`
response = performRequest(router, http.MethodPost, "/api/servers", numericServer, "test-token")
if response.Code != http.StatusCreated {
t.Fatalf("create numeric server: status=%d body=%s", response.Code, response.Body.String())
}
numericServer = `{"username":"0","name":"numeric-updated","type":"kvm","host":"host0","location":"US","password":"secret","monthstart":1}`
response = performRequest(router, http.MethodPut, "/api/servers/0", numericServer, "test-token")
if response.Code != 200 || !strings.Contains(response.Body.String(), "numeric-updated") {
t.Fatalf("update numeric server: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodDelete, "/api/servers/0", "", "test-token")
if response.Code != 200 || !strings.Contains(response.Body.String(), "numeric-updated") {
t.Fatalf("delete numeric server: status=%d body=%s", response.Code, response.Body.String())
}
if app.RuntimeSnapshot().Servers[0].Username != "s01" {
t.Fatal("numeric username operation modified the server at numeric index")
}
response = performRequest(router, http.MethodPost, "/api/servers/s01/reset-traffic", "", "test-token")
if response.Code != http.StatusConflict {
t.Fatalf("offline reset should conflict: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodPost, "/api/reload", "", "test-token")
if response.Code != 200 {
t.Fatalf("reload: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodPost, "/api/restart", "", "test-token")
if response.Code != http.StatusAccepted || !strings.Contains(response.Body.String(), "in-process") {
t.Fatalf("restart: status=%d body=%s", response.Code, response.Body.String())
}
data, err := os.ReadFile(app.opts.ConfigPath)
if err != nil {
t.Fatal(err)
}
var persisted map[string]any
if err := json.Unmarshal(data, &persisted); err != nil {
t.Fatalf("persisted config is invalid: %v", err)
}
}
func TestHTTPRejectsInvalidAndOversizedBodies(t *testing.T) {
app := newTestApp(t, minimalTestConfig())
router := app.router()
response := performRequest(router, http.MethodPost, "/api/servers", `{"name":`, "test-token")
if response.Code != 400 {
t.Fatalf("invalid JSON: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodPost, "/api/servers", `{"name":"x"}`, "test-token")
if response.Code != 400 {
t.Fatalf("missing fields: status=%d body=%s", response.Code, response.Body.String())
}
oversized := `{"name":"` + strings.Repeat("x", maxRequestBody) + `"}`
response = performRequest(router, http.MethodPost, "/api/servers", oversized, "test-token")
if response.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("oversized body: status=%d body=%s", response.Code, response.Body.String())
}
doc := app.ConfigSnapshot()
doc["watchdog"] = []any{map[string]any{"name": "broken", "rule": "cpu >", "interval": 10}}
data, _ := json.Marshal(doc)
response = performRequest(router, http.MethodPut, "/api/config", string(data), "test-token")
if response.Code != 400 {
t.Fatalf("invalid watchdog: status=%d body=%s", response.Code, response.Body.String())
}
}