mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 20:10:21 +00:00
604 lines
14 KiB
Go
604 lines
14 KiB
Go
package approval
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewStore(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, err := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
MaxApprovals: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewStore() error = %v", err)
|
|
}
|
|
if store == nil {
|
|
t.Fatal("NewStore() returned nil store")
|
|
}
|
|
}
|
|
|
|
func TestNewStoreEmptyDataDir(t *testing.T) {
|
|
_, err := NewStore(StoreConfig{
|
|
DataDir: "",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("NewStore() expected error for empty data dir")
|
|
}
|
|
}
|
|
|
|
func TestCreateApproval(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
req := &ApprovalRequest{
|
|
ExecutionID: "exec-1",
|
|
ToolID: "tool-1",
|
|
Command: "systemctl restart nginx",
|
|
TargetType: "host",
|
|
TargetID: "host-1",
|
|
TargetName: "webserver",
|
|
Context: "Service needs restart due to config change",
|
|
}
|
|
|
|
err = store.CreateApproval(req)
|
|
if err != nil {
|
|
t.Fatalf("CreateApproval() error = %v", err)
|
|
}
|
|
|
|
if req.ID == "" {
|
|
t.Error("CreateApproval() did not set ID")
|
|
}
|
|
if req.Status != StatusPending {
|
|
t.Errorf("CreateApproval() status = %v, want %v", req.Status, StatusPending)
|
|
}
|
|
if req.RequestedAt.IsZero() {
|
|
t.Error("CreateApproval() did not set RequestedAt")
|
|
}
|
|
if req.ExpiresAt.IsZero() {
|
|
t.Error("CreateApproval() did not set ExpiresAt")
|
|
}
|
|
}
|
|
|
|
func TestGetApproval(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
req := &ApprovalRequest{
|
|
Command: "apt update",
|
|
}
|
|
store.CreateApproval(req)
|
|
|
|
got, found := store.GetApproval(req.ID)
|
|
if !found {
|
|
t.Fatal("GetApproval() not found")
|
|
}
|
|
if got.Command != "apt update" {
|
|
t.Errorf("GetApproval() command = %v, want %v", got.Command, "apt update")
|
|
}
|
|
|
|
_, found = store.GetApproval("nonexistent")
|
|
if found {
|
|
t.Error("GetApproval() found nonexistent approval")
|
|
}
|
|
}
|
|
|
|
func TestGetPendingApprovals(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
// Create multiple approvals
|
|
for i := 0; i < 3; i++ {
|
|
store.CreateApproval(&ApprovalRequest{
|
|
Command: "test command",
|
|
})
|
|
}
|
|
|
|
pending := store.GetPendingApprovals()
|
|
if len(pending) != 3 {
|
|
t.Errorf("GetPendingApprovals() count = %v, want %v", len(pending), 3)
|
|
}
|
|
}
|
|
|
|
func TestGetApprovalsByExecution(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
store.CreateApproval(&ApprovalRequest{ExecutionID: "exec-1", Command: "cmd-1"})
|
|
store.CreateApproval(&ApprovalRequest{ExecutionID: "exec-1", Command: "cmd-2"})
|
|
store.CreateApproval(&ApprovalRequest{ExecutionID: "exec-2", Command: "cmd-3"})
|
|
|
|
results := store.GetApprovalsByExecution("exec-1")
|
|
if len(results) != 2 {
|
|
t.Fatalf("GetApprovalsByExecution() count = %v, want %v", len(results), 2)
|
|
}
|
|
for _, req := range results {
|
|
if req.ExecutionID != "exec-1" {
|
|
t.Fatalf("GetApprovalsByExecution() returned wrong execution ID: %v", req.ExecutionID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApprove(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
req := &ApprovalRequest{
|
|
Command: "systemctl restart nginx",
|
|
}
|
|
store.CreateApproval(req)
|
|
|
|
got, err := store.Approve(req.ID, "admin")
|
|
if err != nil {
|
|
t.Fatalf("Approve() error = %v", err)
|
|
}
|
|
|
|
if got.Status != StatusApproved {
|
|
t.Errorf("Approve() status = %v, want %v", got.Status, StatusApproved)
|
|
}
|
|
if got.DecidedBy != "admin" {
|
|
t.Errorf("Approve() DecidedBy = %v, want %v", got.DecidedBy, "admin")
|
|
}
|
|
if got.DecidedAt == nil {
|
|
t.Error("Approve() did not set DecidedAt")
|
|
}
|
|
}
|
|
|
|
func TestDeny(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
req := &ApprovalRequest{
|
|
Command: "rm -rf /tmp/data",
|
|
}
|
|
store.CreateApproval(req)
|
|
|
|
got, err := store.Deny(req.ID, "admin", "Too dangerous")
|
|
if err != nil {
|
|
t.Fatalf("Deny() error = %v", err)
|
|
}
|
|
|
|
if got.Status != StatusDenied {
|
|
t.Errorf("Deny() status = %v, want %v", got.Status, StatusDenied)
|
|
}
|
|
if got.DenyReason != "Too dangerous" {
|
|
t.Errorf("Deny() DenyReason = %v, want %v", got.DenyReason, "Too dangerous")
|
|
}
|
|
}
|
|
|
|
func TestGetStats(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
pending := &ApprovalRequest{Command: "pending"}
|
|
store.CreateApproval(pending)
|
|
|
|
approved := &ApprovalRequest{Command: "approved"}
|
|
store.CreateApproval(approved)
|
|
if _, err := store.Approve(approved.ID, "admin"); err != nil {
|
|
t.Fatalf("Approve() error = %v", err)
|
|
}
|
|
|
|
denied := &ApprovalRequest{Command: "denied"}
|
|
store.CreateApproval(denied)
|
|
if _, err := store.Deny(denied.ID, "admin", "no"); err != nil {
|
|
t.Fatalf("Deny() error = %v", err)
|
|
}
|
|
|
|
expired := &ApprovalRequest{
|
|
Command: "expired",
|
|
ExpiresAt: time.Now().Add(-time.Minute),
|
|
}
|
|
store.CreateApproval(expired)
|
|
store.CleanupExpired()
|
|
|
|
if err := store.StoreExecution(&ExecutionState{ID: "exec-1"}); err != nil {
|
|
t.Fatalf("StoreExecution() error = %v", err)
|
|
}
|
|
|
|
stats := store.GetStats()
|
|
if stats["pending"] != 1 || stats["approved"] != 1 || stats["denied"] != 1 || stats["expired"] != 1 {
|
|
t.Fatalf("GetStats() unexpected approval counts: %+v", stats)
|
|
}
|
|
if stats["executions"] != 1 {
|
|
t.Fatalf("GetStats() executions = %v, want %v", stats["executions"], 1)
|
|
}
|
|
}
|
|
|
|
func TestApproveNonexistent(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
_, err = store.Approve("nonexistent", "admin")
|
|
if err == nil {
|
|
t.Error("Approve() expected error for nonexistent approval")
|
|
}
|
|
}
|
|
|
|
func TestApproveAlreadyDecided(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
req := &ApprovalRequest{
|
|
Command: "test",
|
|
}
|
|
store.CreateApproval(req)
|
|
_, _ = store.Deny(req.ID, "admin", "reason")
|
|
|
|
_, err = store.Approve(req.ID, "admin2")
|
|
if err == nil {
|
|
t.Error("Approve() expected error for already decided approval")
|
|
}
|
|
}
|
|
|
|
func TestExecutionState(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
state := &ExecutionState{
|
|
ID: "state-1",
|
|
OriginalRequest: map[string]interface{}{
|
|
"message": "test",
|
|
},
|
|
Messages: []map[string]interface{}{
|
|
{"role": "user", "content": "test"},
|
|
},
|
|
}
|
|
|
|
err = store.StoreExecution(state)
|
|
if err != nil {
|
|
t.Fatalf("StoreExecution() error = %v", err)
|
|
}
|
|
|
|
got, found := store.GetExecution(state.ID)
|
|
if !found {
|
|
t.Fatal("GetExecution() not found")
|
|
}
|
|
if got.ID != state.ID {
|
|
t.Errorf("GetExecution() ID = %v, want %v", got.ID, state.ID)
|
|
}
|
|
|
|
_, found = store.GetExecution("nonexistent")
|
|
if found {
|
|
t.Error("GetExecution() found nonexistent state")
|
|
}
|
|
}
|
|
|
|
func TestDeleteExecution(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
state := &ExecutionState{
|
|
ID: "state-1",
|
|
}
|
|
store.StoreExecution(state)
|
|
|
|
store.DeleteExecution(state.ID)
|
|
|
|
_, found := store.GetExecution(state.ID)
|
|
if found {
|
|
t.Error("DeleteExecution() did not delete state")
|
|
}
|
|
}
|
|
|
|
func TestCleanupExpired(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Millisecond, // Very short timeout
|
|
})
|
|
|
|
// Create an approval that will expire immediately
|
|
req := &ApprovalRequest{
|
|
Command: "test",
|
|
}
|
|
store.CreateApproval(req)
|
|
|
|
// Wait for expiration
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
cleaned := store.CleanupExpired()
|
|
if cleaned != 1 {
|
|
t.Errorf("CleanupExpired() cleaned = %v, want %v", cleaned, 1)
|
|
}
|
|
|
|
got, found := store.GetApproval(req.ID)
|
|
if !found {
|
|
t.Fatal("Approval should still exist after cleanup")
|
|
}
|
|
if got.Status != StatusExpired {
|
|
t.Errorf("CleanupExpired() status = %v, want %v", got.Status, StatusExpired)
|
|
}
|
|
}
|
|
|
|
func TestStartCleanup(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
store.StartCleanup(ctx)
|
|
|
|
// Give cleanup goroutine a moment to start
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Cancel should stop the cleanup loop
|
|
cancel()
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
func TestAssessRiskLevel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
command string
|
|
targetType string
|
|
want RiskLevel
|
|
}{
|
|
{
|
|
name: "high risk rm -rf",
|
|
command: "rm -rf /var/log",
|
|
want: RiskHigh,
|
|
},
|
|
{
|
|
name: "high risk dd",
|
|
command: "dd if=/dev/zero of=/dev/sda",
|
|
want: RiskHigh,
|
|
},
|
|
{
|
|
name: "high risk apt purge",
|
|
command: "apt purge nginx",
|
|
want: RiskHigh,
|
|
},
|
|
{
|
|
name: "medium risk service restart",
|
|
command: "systemctl restart nginx",
|
|
want: RiskMedium,
|
|
},
|
|
{
|
|
name: "medium risk docker restart",
|
|
command: "docker restart mycontainer",
|
|
want: RiskMedium,
|
|
},
|
|
{
|
|
name: "medium risk apt install",
|
|
command: "apt install htop",
|
|
want: RiskMedium,
|
|
},
|
|
{
|
|
name: "low risk diagnostic",
|
|
command: "df -h",
|
|
want: RiskLow,
|
|
},
|
|
{
|
|
name: "low risk logs",
|
|
command: "journalctl -u nginx",
|
|
want: RiskLow,
|
|
},
|
|
{
|
|
name: "low risk status check",
|
|
command: "systemctl status nginx",
|
|
want: RiskLow,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := AssessRiskLevel(tt.command, tt.targetType)
|
|
if got != tt.want {
|
|
t.Errorf("AssessRiskLevel(%q) = %v, want %v", tt.command, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStorePersistence(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Create store and add data
|
|
store1, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Hour,
|
|
})
|
|
|
|
req := &ApprovalRequest{
|
|
Command: "test command",
|
|
TargetID: "host-1",
|
|
TargetName: "webserver",
|
|
}
|
|
store1.CreateApproval(req)
|
|
approvalID := req.ID
|
|
|
|
state := &ExecutionState{
|
|
ID: "exec-1",
|
|
OriginalRequest: map[string]interface{}{
|
|
"message": "test",
|
|
},
|
|
}
|
|
store1.StoreExecution(state)
|
|
|
|
// Flush debounced writes to disk immediately
|
|
store1.Flush()
|
|
|
|
// Create new store from same directory
|
|
store2, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Hour,
|
|
})
|
|
|
|
// Verify data was loaded
|
|
loadedApproval, found := store2.GetApproval(approvalID)
|
|
if !found {
|
|
t.Fatal("Approval not persisted")
|
|
}
|
|
if loadedApproval.Command != "test command" {
|
|
t.Errorf("Persisted approval command = %v, want %v", loadedApproval.Command, "test command")
|
|
}
|
|
|
|
loadedState, found := store2.GetExecution("exec-1")
|
|
if !found {
|
|
t.Fatal("Execution state not persisted")
|
|
}
|
|
if loadedState.ID != "exec-1" {
|
|
t.Errorf("Persisted state ID = %v, want %v", loadedState.ID, "exec-1")
|
|
}
|
|
}
|
|
|
|
func TestMaxApprovals(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Hour,
|
|
MaxApprovals: 2,
|
|
})
|
|
|
|
// Create first two approvals - should succeed
|
|
store.CreateApproval(&ApprovalRequest{Command: "test1"})
|
|
store.CreateApproval(&ApprovalRequest{Command: "test2"})
|
|
|
|
// Third should fail
|
|
err = store.CreateApproval(&ApprovalRequest{Command: "test3"})
|
|
if err == nil {
|
|
t.Error("CreateApproval() expected error when max approvals reached")
|
|
}
|
|
}
|
|
|
|
func TestGlobalStore(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store, _ := NewStore(StoreConfig{
|
|
DataDir: tmpDir,
|
|
DefaultTimeout: 1 * time.Minute,
|
|
})
|
|
|
|
SetStore(store)
|
|
got := GetStore()
|
|
|
|
if got != store {
|
|
t.Error("GetStore() did not return the set store")
|
|
}
|
|
|
|
// Reset global store
|
|
SetStore(nil)
|
|
}
|