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, "") 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) }