Files
Charon/backend/internal/api/handlers/permission_helpers_test.go
2026-03-04 18:34:49 +00:00

202 lines
5.9 KiB
Go

package handlers
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/Wikid82/charon/backend/internal/services"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func newTestContextWithRequest() (*gin.Context, *httptest.ResponseRecorder) {
rec := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(rec)
ctx.Request = httptest.NewRequest(http.MethodGet, "/", http.NoBody)
return ctx, rec
}
func TestRequireAdmin(t *testing.T) {
t.Parallel()
t.Run("admin allowed", func(t *testing.T) {
t.Parallel()
ctx, _ := newTestContextWithRequest()
ctx.Set("role", "admin")
assert.True(t, requireAdmin(ctx))
})
t.Run("non-admin forbidden", func(t *testing.T) {
t.Parallel()
ctx, rec := newTestContextWithRequest()
ctx.Set("role", "user")
assert.False(t, requireAdmin(ctx))
assert.Equal(t, http.StatusForbidden, rec.Code)
assert.Contains(t, rec.Body.String(), "admin privileges required")
})
}
func TestIsAdmin(t *testing.T) {
t.Parallel()
ctx, _ := newTestContextWithRequest()
assert.False(t, isAdmin(ctx))
ctx.Set("role", "admin")
assert.True(t, isAdmin(ctx))
ctx.Set("role", "user")
assert.False(t, isAdmin(ctx))
}
func TestPermissionErrorMessage(t *testing.T) {
t.Parallel()
assert.Equal(t, "database is read-only", permissionErrorMessage("permissions_db_readonly"))
assert.Equal(t, "database is locked", permissionErrorMessage("permissions_db_locked"))
assert.Equal(t, "filesystem is read-only", permissionErrorMessage("permissions_readonly"))
assert.Equal(t, "permission denied", permissionErrorMessage("permissions_write_denied"))
assert.Equal(t, "permission error", permissionErrorMessage("something_else"))
}
func TestBuildPermissionHelp(t *testing.T) {
t.Parallel()
emptyPathHelp := buildPermissionHelp("")
assert.Contains(t, emptyPathHelp, "chown -R")
assert.Contains(t, emptyPathHelp, "<path-to-volume>")
help := buildPermissionHelp("/data/path")
assert.Contains(t, help, "chown -R")
assert.Contains(t, help, "/data/path")
}
func TestRespondPermissionError_UnmappedReturnsFalse(t *testing.T) {
t.Parallel()
ctx, rec := newTestContextWithRequest()
ok := respondPermissionError(ctx, nil, "action", errors.New("not mapped"), "/tmp")
assert.False(t, ok)
assert.Equal(t, http.StatusOK, rec.Code)
}
func TestRespondPermissionError_NonAdminMappedError(t *testing.T) {
t.Parallel()
ctx, rec := newTestContextWithRequest()
ctx.Set("role", "user")
ok := respondPermissionError(ctx, nil, "save_failed", errors.New("permission denied"), "/data")
require.True(t, ok)
assert.Equal(t, http.StatusInternalServerError, rec.Code)
assert.Contains(t, rec.Body.String(), "permission denied")
assert.Contains(t, rec.Body.String(), "permissions_write_denied")
assert.Contains(t, rec.Body.String(), "contact an administrator")
}
func TestRespondPermissionError_AdminWithAudit(t *testing.T) {
t.Parallel()
dbName := "file:" + t.Name() + "?mode=memory&cache=shared"
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SecurityAudit{}))
securityService := services.NewSecurityService(db)
t.Cleanup(func() {
securityService.Close()
})
ctx, rec := newTestContextWithRequest()
ctx.Set("role", "admin")
ctx.Set("userID", uint(77))
ok := respondPermissionError(ctx, securityService, "settings_save_failed", errors.New("database is locked"), "/var/lib/charon")
require.True(t, ok)
assert.Equal(t, http.StatusInternalServerError, rec.Code)
assert.Contains(t, rec.Body.String(), "database is locked")
assert.Contains(t, rec.Body.String(), "permissions_db_locked")
assert.Contains(t, rec.Body.String(), "/var/lib/charon")
securityService.Flush()
var audits []models.SecurityAudit
require.NoError(t, db.Find(&audits).Error)
require.NotEmpty(t, audits)
assert.Equal(t, "77", audits[0].Actor)
assert.Equal(t, "settings_save_failed", audits[0].Action)
assert.Equal(t, "permissions", audits[0].EventCategory)
}
func TestLogPermissionAudit_NoService(t *testing.T) {
t.Parallel()
ctx, _ := newTestContextWithRequest()
assert.NotPanics(t, func() {
logPermissionAudit(nil, ctx, "action", "permissions_write_denied", "/tmp", true)
})
}
func TestLogPermissionAudit_ActorFallback(t *testing.T) {
t.Parallel()
dbName := "file:" + t.Name() + "?mode=memory&cache=shared"
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SecurityAudit{}))
securityService := services.NewSecurityService(db)
t.Cleanup(func() {
securityService.Close()
})
ctx, _ := newTestContextWithRequest()
logPermissionAudit(securityService, ctx, "backup_create_failed", "permissions_readonly", "", false)
securityService.Flush()
var audit models.SecurityAudit
require.NoError(t, db.First(&audit).Error)
assert.Equal(t, "unknown", audit.Actor)
assert.Equal(t, "backup_create_failed", audit.Action)
assert.Equal(t, "permissions", audit.EventCategory)
assert.Contains(t, audit.Details, fmt.Sprintf("\"admin\":%v", false))
}
func TestRequireAuthenticatedAdmin_NoUserID(t *testing.T) {
t.Parallel()
ctx, rec := newTestContextWithRequest()
result := requireAuthenticatedAdmin(ctx)
assert.False(t, result)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
assert.Contains(t, rec.Body.String(), "Authorization header required")
}
func TestRequireAuthenticatedAdmin_UserIDPresentAndAdmin(t *testing.T) {
t.Parallel()
ctx, _ := newTestContextWithRequest()
ctx.Set("userID", uint(1))
ctx.Set("role", "admin")
result := requireAuthenticatedAdmin(ctx)
assert.True(t, result)
}
func TestRequireAuthenticatedAdmin_UserIDPresentButNotAdmin(t *testing.T) {
t.Parallel()
ctx, rec := newTestContextWithRequest()
ctx.Set("userID", uint(1))
ctx.Set("role", "user")
result := requireAuthenticatedAdmin(ctx)
assert.False(t, result)
assert.Equal(t, http.StatusForbidden, rec.Code)
}