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