permission_test.go

  1package permission
  2
  3import (
  4	"sync"
  5	"testing"
  6
  7	"github.com/stretchr/testify/assert"
  8	"github.com/stretchr/testify/require"
  9)
 10
 11func TestPermissionService_AllowedCommands(t *testing.T) {
 12	tests := []struct {
 13		name         string
 14		allowedTools []string
 15		toolName     string
 16		action       string
 17		expected     bool
 18	}{
 19		{
 20			name:         "tool in allowlist",
 21			allowedTools: []string{"bash", "view"},
 22			toolName:     "bash",
 23			action:       "execute",
 24			expected:     true,
 25		},
 26		{
 27			name:         "tool:action in allowlist",
 28			allowedTools: []string{"bash:execute", "edit:create"},
 29			toolName:     "bash",
 30			action:       "execute",
 31			expected:     true,
 32		},
 33		{
 34			name:         "tool not in allowlist",
 35			allowedTools: []string{"view", "ls"},
 36			toolName:     "bash",
 37			action:       "execute",
 38			expected:     false,
 39		},
 40		{
 41			name:         "tool:action not in allowlist",
 42			allowedTools: []string{"bash:read", "edit:create"},
 43			toolName:     "bash",
 44			action:       "execute",
 45			expected:     false,
 46		},
 47		{
 48			name:         "empty allowlist",
 49			allowedTools: []string{},
 50			toolName:     "bash",
 51			action:       "execute",
 52			expected:     false,
 53		},
 54	}
 55
 56	for _, tt := range tests {
 57		t.Run(tt.name, func(t *testing.T) {
 58			service := NewPermissionService("/tmp", false, tt.allowedTools)
 59
 60			// Create a channel to capture the permission request
 61			// Since we're testing the allowlist logic, we need to simulate the request
 62			ps := service.(*permissionService)
 63
 64			// Test the allowlist logic directly
 65			commandKey := tt.toolName + ":" + tt.action
 66			allowed := false
 67			for _, cmd := range ps.allowedTools {
 68				if cmd == commandKey || cmd == tt.toolName {
 69					allowed = true
 70					break
 71				}
 72			}
 73
 74			if allowed != tt.expected {
 75				t.Errorf("expected %v, got %v for tool %s action %s with allowlist %v",
 76					tt.expected, allowed, tt.toolName, tt.action, tt.allowedTools)
 77			}
 78		})
 79	}
 80}
 81
 82func TestPermissionService_SkipMode(t *testing.T) {
 83	service := NewPermissionService("/tmp", true, []string{})
 84
 85	result, err := service.Request(t.Context(), CreatePermissionRequest{
 86		SessionID:   "test-session",
 87		ToolName:    "bash",
 88		Action:      "execute",
 89		Description: "test command",
 90		Path:        "/tmp",
 91	})
 92	if err != nil {
 93		t.Errorf("unexpected error: %v", err)
 94	}
 95	if !result {
 96		t.Error("expected permission to be granted in skip mode")
 97	}
 98}
 99
100func TestPermissionService_SequentialProperties(t *testing.T) {
101	t.Run("Sequential permission requests with persistent grants", func(t *testing.T) {
102		service := NewPermissionService("/tmp", false, []string{})
103
104		req1 := CreatePermissionRequest{
105			SessionID:   "session1",
106			ToolName:    "file_tool",
107			Description: "Read file",
108			Action:      "read",
109			Params:      map[string]string{"file": "test.txt"},
110			Path:        "/tmp/test.txt",
111		}
112
113		var result1 bool
114		var wg sync.WaitGroup
115		wg.Add(1)
116
117		events := service.Subscribe(t.Context())
118
119		go func() {
120			defer wg.Done()
121			result1, _ = service.Request(t.Context(), req1)
122		}()
123
124		var permissionReq PermissionRequest
125		event := <-events
126
127		permissionReq = event.Payload
128		service.GrantPersistent(permissionReq)
129
130		wg.Wait()
131		assert.True(t, result1, "First request should be granted")
132
133		// Second identical request should be automatically approved due to persistent permission
134		req2 := CreatePermissionRequest{
135			SessionID:   "session1",
136			ToolName:    "file_tool",
137			Description: "Read file again",
138			Action:      "read",
139			Params:      map[string]string{"file": "test.txt"},
140			Path:        "/tmp/test.txt",
141		}
142		result2, err := service.Request(t.Context(), req2)
143		require.NoError(t, err)
144		assert.True(t, result2, "Second request should be auto-approved")
145	})
146	t.Run("Sequential requests with temporary grants", func(t *testing.T) {
147		service := NewPermissionService("/tmp", false, []string{})
148
149		req := CreatePermissionRequest{
150			SessionID:   "session2",
151			ToolName:    "file_tool",
152			Description: "Write file",
153			Action:      "write",
154			Params:      map[string]string{"file": "test.txt"},
155			Path:        "/tmp/test.txt",
156		}
157
158		events := service.Subscribe(t.Context())
159		var result1 bool
160		var wg sync.WaitGroup
161
162		wg.Go(func() {
163			result1, _ = service.Request(t.Context(), req)
164		})
165
166		var permissionReq PermissionRequest
167		event := <-events
168		permissionReq = event.Payload
169
170		service.Grant(permissionReq)
171		wg.Wait()
172		assert.True(t, result1, "First request should be granted")
173
174		var result2 bool
175
176		wg.Go(func() {
177			result2, _ = service.Request(t.Context(), req)
178		})
179
180		event = <-events
181		permissionReq = event.Payload
182		service.Deny(permissionReq)
183		wg.Wait()
184		assert.False(t, result2, "Second request should be denied")
185	})
186	t.Run("Concurrent requests with different outcomes", func(t *testing.T) {
187		service := NewPermissionService("/tmp", false, []string{})
188
189		events := service.Subscribe(t.Context())
190
191		var wg sync.WaitGroup
192		results := make([]bool, 3)
193
194		requests := []CreatePermissionRequest{
195			{
196				SessionID:   "concurrent1",
197				ToolName:    "tool1",
198				Action:      "action1",
199				Path:        "/tmp/file1.txt",
200				Description: "First concurrent request",
201			},
202			{
203				SessionID:   "concurrent2",
204				ToolName:    "tool2",
205				Action:      "action2",
206				Path:        "/tmp/file2.txt",
207				Description: "Second concurrent request",
208			},
209			{
210				SessionID:   "concurrent3",
211				ToolName:    "tool3",
212				Action:      "action3",
213				Path:        "/tmp/file3.txt",
214				Description: "Third concurrent request",
215			},
216		}
217
218		for i, req := range requests {
219			wg.Add(1)
220			go func(index int, request CreatePermissionRequest) {
221				defer wg.Done()
222				result, _ := service.Request(t.Context(), request)
223				results[index] = result
224			}(i, req)
225		}
226
227		for range 3 {
228			event := <-events
229			switch event.Payload.ToolName {
230			case "tool1":
231				service.Grant(event.Payload)
232			case "tool2":
233				service.GrantPersistent(event.Payload)
234			case "tool3":
235				service.Deny(event.Payload)
236			}
237		}
238		wg.Wait()
239		grantedCount := 0
240		for _, result := range results {
241			if result {
242				grantedCount++
243			}
244		}
245
246		assert.Equal(t, 2, grantedCount, "Should have 2 granted and 1 denied")
247		secondReq := requests[1]
248		secondReq.Description = "Repeat of second request"
249		result, err := service.Request(t.Context(), secondReq)
250		require.NoError(t, err)
251		assert.True(t, result, "Repeated request should be auto-approved due to persistent permission")
252	})
253}