chore: add unit tests for system permissions handler and proxy host service validation

This commit is contained in:
GitHub Actions
2026-02-16 05:41:49 +00:00
parent 943fb2df40
commit 3a25782a11
3 changed files with 292 additions and 0 deletions

View File

@@ -1,10 +1,14 @@
package handlers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"syscall"
"testing"
"github.com/gin-gonic/gin"
@@ -105,3 +109,182 @@ func TestSystemPermissionsHandler_RepairPermissions_NonRoot(t *testing.T) {
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
require.Equal(t, "permissions_non_root", payload["error_code"])
}
func TestSystemPermissionsHandler_HelperFunctions(t *testing.T) {
t.Run("normalizePath", func(t *testing.T) {
clean, code := normalizePath("/tmp/example")
require.Equal(t, "/tmp/example", clean)
require.Empty(t, code)
clean, code = normalizePath("")
require.Empty(t, clean)
require.Equal(t, "permissions_invalid_path", code)
clean, code = normalizePath("relative/path")
require.Empty(t, clean)
require.Equal(t, "permissions_invalid_path", code)
})
t.Run("containsParentReference", func(t *testing.T) {
require.True(t, containsParentReference(".."))
require.True(t, containsParentReference("../secrets"))
require.True(t, containsParentReference("/var/../etc"))
require.True(t, containsParentReference("/var/log/.."))
require.False(t, containsParentReference("/var/log/charon"))
})
t.Run("isWithinAllowlist", func(t *testing.T) {
allowlist := []string{"/app/data", "/config"}
require.True(t, isWithinAllowlist("/app/data/charon.db", allowlist))
require.True(t, isWithinAllowlist("/config/caddy", allowlist))
require.False(t, isWithinAllowlist("/etc/passwd", allowlist))
})
t.Run("targetMode", func(t *testing.T) {
require.Equal(t, "0700", targetMode(true, false))
require.Equal(t, "0770", targetMode(true, true))
require.Equal(t, "0600", targetMode(false, false))
require.Equal(t, "0660", targetMode(false, true))
})
t.Run("parseMode", func(t *testing.T) {
mode, err := parseMode("0640")
require.NoError(t, err)
require.Equal(t, os.FileMode(0640), mode)
_, err = parseMode("")
require.Error(t, err)
_, err = parseMode("invalid")
require.Error(t, err)
})
t.Run("mapRepairErrorCode", func(t *testing.T) {
require.Equal(t, "", mapRepairErrorCode(nil))
require.Equal(t, "permissions_readonly", mapRepairErrorCode(syscall.EROFS))
require.Equal(t, "permissions_write_denied", mapRepairErrorCode(syscall.EACCES))
require.Equal(t, "permissions_repair_failed", mapRepairErrorCode(syscall.EINVAL))
})
}
func TestSystemPermissionsHandler_PathHasSymlink(t *testing.T) {
root := t.TempDir()
realDir := filepath.Join(root, "real")
require.NoError(t, os.Mkdir(realDir, 0o755))
plainPath := filepath.Join(realDir, "file.txt")
require.NoError(t, os.WriteFile(plainPath, []byte("ok"), 0o644))
hasSymlink, err := pathHasSymlink(plainPath)
require.NoError(t, err)
require.False(t, hasSymlink)
linkDir := filepath.Join(root, "link")
require.NoError(t, os.Symlink(realDir, linkDir))
symlinkedPath := filepath.Join(linkDir, "file.txt")
hasSymlink, err = pathHasSymlink(symlinkedPath)
require.NoError(t, err)
require.True(t, hasSymlink)
_, err = pathHasSymlink(filepath.Join(root, "missing", "file.txt"))
require.Error(t, err)
}
func TestSystemPermissionsHandler_RepairPermissions_DisabledWhenNotSingleContainer(t *testing.T) {
gin.SetMode(gin.TestMode)
h := NewSystemPermissionsHandler(config.Config{SingleContainer: false}, nil, stubPermissionChecker{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "admin")
c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"paths":["/tmp"]}`))
c.Request.Header.Set("Content-Type", "application/json")
h.RepairPermissions(c)
require.Equal(t, http.StatusForbidden, w.Code)
var payload map[string]string
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
require.Equal(t, "permissions_repair_disabled", payload["error_code"])
}
func TestSystemPermissionsHandler_RepairPermissions_InvalidJSON(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("test requires root execution")
}
gin.SetMode(gin.TestMode)
root := t.TempDir()
dataDir := filepath.Join(root, "data")
require.NoError(t, os.MkdirAll(dataDir, 0o755))
cfg := config.Config{
SingleContainer: true,
DatabasePath: filepath.Join(dataDir, "charon.db"),
ConfigRoot: dataDir,
CaddyLogDir: dataDir,
CrowdSecLogDir: dataDir,
PluginsDir: filepath.Join(root, "plugins"),
}
h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "admin")
c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"paths":`))
c.Request.Header.Set("Content-Type", "application/json")
h.RepairPermissions(c)
require.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSystemPermissionsHandler_RepairPermissions_Success(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("test requires root execution")
}
gin.SetMode(gin.TestMode)
root := t.TempDir()
dataDir := filepath.Join(root, "data")
require.NoError(t, os.MkdirAll(dataDir, 0o755))
targetFile := filepath.Join(dataDir, "repair-target.txt")
require.NoError(t, os.WriteFile(targetFile, []byte("repair"), 0o644))
cfg := config.Config{
SingleContainer: true,
DatabasePath: filepath.Join(dataDir, "charon.db"),
ConfigRoot: dataDir,
CaddyLogDir: dataDir,
CrowdSecLogDir: dataDir,
PluginsDir: filepath.Join(root, "plugins"),
}
h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})
body := fmt.Sprintf(`{"paths":[%q],"group_mode":false}`, targetFile)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "admin")
c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
h.RepairPermissions(c)
require.Equal(t, http.StatusOK, w.Code)
var payload struct {
Paths []permissionsRepairResult `json:"paths"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
require.Len(t, payload.Paths, 1)
require.Equal(t, targetFile, payload.Paths[0].Path)
require.NotEqual(t, "error", payload.Paths[0].Status)
}

View File

@@ -198,3 +198,33 @@ func TestProxyHostService_DNSChallengeValidation(t *testing.T) {
assert.Equal(t, "localhost", persisted.ForwardHost)
})
}
func TestProxyHostService_ValidateHostname(t *testing.T) {
db := setupProxyHostTestDB(t)
service := NewProxyHostService(db)
tests := []struct {
name string
host string
wantErr bool
}{
{name: "plain hostname", host: "example.com", wantErr: false},
{name: "hostname with scheme", host: "https://example.com", wantErr: false},
{name: "hostname with port", host: "example.com:8080", wantErr: false},
{name: "ipv4 address", host: "127.0.0.1", wantErr: false},
{name: "bracketed ipv6 with port", host: "[::1]:443", wantErr: false},
{name: "docker style underscore", host: "my_service", wantErr: false},
{name: "invalid character", host: "invalid$host", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := service.ValidateHostname(tt.host)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

View File

@@ -3,6 +3,7 @@ package util
import (
"errors"
"fmt"
"os"
"syscall"
"testing"
)
@@ -55,3 +56,81 @@ func TestIsSQLiteReadOnlyError(t *testing.T) {
t.Fatalf("expected SQLITE_READONLY to be detected")
}
}
func TestMapDiagnosticErrorCode(t *testing.T) {
tests := []struct {
name string
err error
want string
}{
{name: "nil", err: nil, want: ""},
{name: "not found", err: os.ErrNotExist, want: "permissions_missing_path"},
{name: "readonly", err: syscall.EROFS, want: "permissions_readonly"},
{name: "permission denied", err: syscall.EACCES, want: "permissions_write_denied"},
{name: "other", err: errors.New("boom"), want: "permissions_write_failed"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := MapDiagnosticErrorCode(tt.err); got != tt.want {
t.Fatalf("MapDiagnosticErrorCode() = %q, want %q", got, tt.want)
}
})
}
}
func TestCheckPathPermissions(t *testing.T) {
t.Run("missing path", func(t *testing.T) {
result := CheckPathPermissions("/definitely/missing/path", "rw")
if result.Exists {
t.Fatalf("expected missing path to not exist")
}
if result.ErrorCode != "permissions_missing_path" {
t.Fatalf("expected permissions_missing_path, got %q", result.ErrorCode)
}
})
t.Run("writable file", func(t *testing.T) {
tempFile, err := os.CreateTemp(t.TempDir(), "perm-file-*.txt")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
if closeErr := tempFile.Close(); closeErr != nil {
t.Fatalf("close temp file: %v", closeErr)
}
result := CheckPathPermissions(tempFile.Name(), "rw")
if !result.Exists {
t.Fatalf("expected file to exist")
}
if !result.Writable {
t.Fatalf("expected file to be writable, got error: %s", result.Error)
}
})
t.Run("writable directory", func(t *testing.T) {
dir := t.TempDir()
result := CheckPathPermissions(dir, "rwx")
if !result.Exists {
t.Fatalf("expected directory to exist")
}
if !result.Writable {
t.Fatalf("expected directory to be writable, got error: %s", result.Error)
}
})
t.Run("no write required", func(t *testing.T) {
tempFile, err := os.CreateTemp(t.TempDir(), "perm-read-*.txt")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
if closeErr := tempFile.Close(); closeErr != nil {
t.Fatalf("close temp file: %v", closeErr)
}
result := CheckPathPermissions(tempFile.Name(), "r")
if result.Writable {
t.Fatalf("expected writable=false when write permission is not required")
}
})
}