internal/server/caddy_servers_test.go
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: Apache-2.0
package server
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/X4Applegate/caddyui/internal/auth"
appdb "github.com/X4Applegate/caddyui/internal/db"
"github.com/X4Applegate/caddyui/internal/models"
)
func TestAPIV1ListServersReturnsSafeFleetMetadata(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
id, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "EDGE",
AdminURL: "http://caddy-edge:2019",
Type: models.CaddyServerTypeManaged,
Tags: "edge, production",
AdminUsername: "secret-user",
AdminPassword: "secret-password",
PublicIP: "203.0.113.10",
})
if err != nil {
t.Fatal(err)
}
if err := models.SetCaddyServerVersion(conn, id, "v2.10.0"); err != nil {
t.Fatal(err)
}
contactedAt := time.Date(2026, 7, 30, 12, 34, 56, 0, time.UTC)
if err := models.SetCaddyServerStatus(conn, id, models.CaddyServerStatusOnline, &contactedAt); err != nil {
t.Fatal(err)
}
s := &Server{DB: conn}
req := httptest.NewRequest(http.MethodGet, "/api/v1/servers", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ContextUserKey, &models.User{
ID: 1, Email: "admin@example.com", IsAdmin: true, Role: models.RoleAdmin,
}))
rec := httptest.NewRecorder()
s.apiV1ListServers(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
}
var rows []map[string]any
if err := json.NewDecoder(rec.Body).Decode(&rows); err != nil {
t.Fatal(err)
}
if len(rows) != 1 {
t.Fatalf("servers = %d, want 1", len(rows))
}
row := rows[0]
if row["name"] != "EDGE" || row["type"] != "managed" || row["status"] != "online" || row["version"] != "v2.10.0" {
t.Fatalf("unexpected server metadata: %#v", row)
}
if row["last_contact_at"] != "2026-07-30T12:34:56Z" {
t.Fatalf("last_contact_at = %#v", row["last_contact_at"])
}
tags, ok := row["tags"].([]any)
if !ok || len(tags) != 2 || tags[0] != "edge" || tags[1] != "production" {
t.Fatalf("tags = %#v", row["tags"])
}
for _, secretField := range []string{"admin_username", "admin_password", "public_ip"} {
if _, found := row[secretField]; found {
t.Fatalf("response leaked %q: %#v", secretField, row)
}
}
}
func TestAPIV1ListServersAllowsAuthenticatedNonAdmin(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
if _, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "Primary", AdminURL: "http://caddy:2019", Type: models.CaddyServerTypeManaged,
}); err != nil {
t.Fatal(err)
}
s := &Server{DB: conn}
req := httptest.NewRequest(http.MethodGet, "/api/v1/servers", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ContextUserKey, &models.User{
ID: 2, Email: "viewer@example.com", Role: models.RoleView,
}))
rec := httptest.NewRecorder()
s.apiV1ListServers(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
}
func TestValidateProxyAdvancedUsesSelectedFleetServer(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
var primaryAdaptCalls int
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/adapt" {
primaryAdaptCalls++
http.Error(w, "wrong server", http.StatusBadGateway)
return
}
w.WriteHeader(http.StatusOK)
}))
defer primary.Close()
var selectedAdaptCalls int
selected := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/adapt" {
http.Error(w, "unexpected path", http.StatusNotFound)
return
}
selectedAdaptCalls++
body, _ := io.ReadAll(r.Body)
if !strings.Contains(string(body), "header X-Test value") {
t.Fatalf("adapt body = %q, want advanced config snippet", string(body))
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"result":{"apps":{"http":{"servers":{"srv0":{"routes":[{"handle":[{"handler":"headers"}]}]}}}}}}`)
}))
defer selected.Close()
if _, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "Primary",
AdminURL: primary.URL,
Type: models.CaddyServerTypeManaged,
}); err != nil {
t.Fatal(err)
}
selectedID, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "Remote",
AdminURL: selected.URL,
Type: models.CaddyServerTypeManaged,
})
if err != nil {
t.Fatal(err)
}
s := &Server{DB: conn}
req := httptest.NewRequest(http.MethodPost, "/proxy-hosts", nil)
req.AddCookie(&http.Cookie{Name: serverCookie, Value: strconv.FormatInt(selectedID, 10)})
if errMsg := s.validateProxyAdvanced(s.caddyForRequest(req), &models.ProxyHost{
AdvancedConfig: "header X-Test value",
}); errMsg != "" {
t.Fatalf("validateProxyAdvanced() = %q, want success", errMsg)
}
if primaryAdaptCalls != 0 {
t.Fatalf("primary adapt calls = %d, want 0", primaryAdaptCalls)
}
if selectedAdaptCalls != 1 {
t.Fatalf("selected adapt calls = %d, want 1", selectedAdaptCalls)
}
}