@@ -6,12 +6,38 @@ import (
6
6
"strings"
7
7
"testing"
8
8
9
+ "github.com/ActiveState/termtest/xpty"
9
10
"github.com/spf13/cobra"
10
11
"github.com/spf13/viper"
11
12
"github.com/stretchr/testify/assert"
12
13
"github.com/stretchr/testify/require"
13
14
)
14
15
16
+ // setupCommandWithPTY configures a cobra command to use xpty for output capture
17
+ // and returns the xpty instance for reading captured output.
18
+ // The caller is responsible for closing the returned xpty.
19
+ func setupCommandWithPTY (t * testing.T , cmd * cobra.Command ) * xpty.Xpty {
20
+ t .Helper ()
21
+
22
+ // Create virtual PTY with 100x100 dimensions
23
+ xp , err := xpty .New (100 , 100 , false )
24
+ require .NoError (t , err , "failed to create xpty" )
25
+
26
+ // Configure command to write to the PTY
27
+ ptyWriter := xp .TerminalInPipe ()
28
+ cmd .SetOut (ptyWriter )
29
+ cmd .SetErr (ptyWriter )
30
+
31
+ // Setup cleanup to close PTY when test completes
32
+ t .Cleanup (func () {
33
+ if err := xp .Close (); err != nil {
34
+ t .Logf ("Warning: failed to close xpty: %v" , err )
35
+ }
36
+ })
37
+
38
+ return xp
39
+ }
40
+
15
41
func TestParseAgentType (t * testing.T ) {
16
42
tests := []struct {
17
43
firstArg string
@@ -141,17 +167,16 @@ func TestServerCmd_AllArgs_Defaults(t *testing.T) {
141
167
{"chat-base-path default" , FlagChatBasePath , "/chat" , func () any { return viper .GetString (FlagChatBasePath ) }},
142
168
{"term-width default" , FlagTermWidth , uint16 (80 ), func () any { return viper .GetUint16 (FlagTermWidth ) }},
143
169
{"term-height default" , FlagTermHeight , uint16 (1000 ), func () any { return viper .GetUint16 (FlagTermHeight ) }},
170
+ {"allowed-hosts default" , FlagAllowedHosts , []string {"localhost:3284" , "localhost:3000" , "localhost:3001" }, func () any { return viper .GetStringSlice (FlagAllowedHosts ) }},
144
171
}
145
172
146
173
for _ , tt := range tests {
147
174
t .Run (tt .name , func (t * testing.T ) {
148
175
isolateViper (t )
149
176
serverCmd := CreateServerCmd ()
150
- cmd := & cobra.Command {}
151
- cmd .AddCommand (serverCmd )
152
-
153
- // Execute with no args to get defaults
154
- serverCmd .SetArgs ([]string {"--help" }) // Use help to avoid actual execution
177
+ setupCommandWithPTY (t , serverCmd )
178
+ // Execute with --exit to get defaults
179
+ serverCmd .SetArgs ([]string {"--exit" , "dummy-command" })
155
180
if err := serverCmd .Execute (); err != nil {
156
181
t .Fatalf ("Failed to execute server command: %v" , err )
157
182
}
@@ -175,6 +200,7 @@ func TestServerCmd_AllEnvVars(t *testing.T) {
175
200
{"AGENTAPI_CHAT_BASE_PATH" , "AGENTAPI_CHAT_BASE_PATH" , "/api" , "/api" , func () any { return viper .GetString (FlagChatBasePath ) }},
176
201
{"AGENTAPI_TERM_WIDTH" , "AGENTAPI_TERM_WIDTH" , "120" , uint16 (120 ), func () any { return viper .GetUint16 (FlagTermWidth ) }},
177
202
{"AGENTAPI_TERM_HEIGHT" , "AGENTAPI_TERM_HEIGHT" , "500" , uint16 (500 ), func () any { return viper .GetUint16 (FlagTermHeight ) }},
203
+ {"AGENTAPI_ALLOWED_HOSTS" , "AGENTAPI_ALLOWED_HOSTS" , "localhost:3284 localhost:3285" , []string {"localhost:3284" , "localhost:3285" }, func () any { return viper .GetStringSlice (FlagAllowedHosts ) }},
178
204
}
179
205
180
206
for _ , tt := range tests {
@@ -183,10 +209,8 @@ func TestServerCmd_AllEnvVars(t *testing.T) {
183
209
t .Setenv (tt .envVar , tt .envValue )
184
210
185
211
serverCmd := CreateServerCmd ()
186
- cmd := & cobra.Command {}
187
- cmd .AddCommand (serverCmd )
188
-
189
- serverCmd .SetArgs ([]string {"--help" })
212
+ setupCommandWithPTY (t , serverCmd )
213
+ serverCmd .SetArgs ([]string {"--exit" , "dummy-command" })
190
214
if err := serverCmd .Execute (); err != nil {
191
215
t .Fatalf ("Failed to execute server command: %v" , err )
192
216
}
@@ -254,9 +278,9 @@ func TestServerCmd_ArgsPrecedenceOverEnv(t *testing.T) {
254
278
isolateViper (t )
255
279
t .Setenv (tt .envVar , tt .envValue )
256
280
257
- // Mock execution to test arg parsing without running server
258
- args := append (tt .args , "--help" )
281
+ args := append (tt .args , "--exit" , "dummy-command" )
259
282
serverCmd := CreateServerCmd ()
283
+ setupCommandWithPTY (t , serverCmd )
260
284
serverCmd .SetArgs (args )
261
285
if err := serverCmd .Execute (); err != nil {
262
286
t .Fatalf ("Failed to execute server command: %v" , err )
@@ -277,7 +301,8 @@ func TestMixed_ConfigurationScenarios(t *testing.T) {
277
301
278
302
// Set some CLI args
279
303
serverCmd := CreateServerCmd ()
280
- serverCmd .SetArgs ([]string {"--port" , "9999" , "--print-openapi" , "--help" })
304
+ setupCommandWithPTY (t , serverCmd )
305
+ serverCmd .SetArgs ([]string {"--port" , "9999" , "--print-openapi" , "--exit" , "dummy-command" })
281
306
if err := serverCmd .Execute (); err != nil {
282
307
t .Fatalf ("Failed to execute server command: %v" , err )
283
308
}
@@ -291,3 +316,118 @@ func TestMixed_ConfigurationScenarios(t *testing.T) {
291
316
assert .Equal (t , uint16 (1000 ), viper .GetUint16 (FlagTermHeight )) // default
292
317
})
293
318
}
319
+
320
+ func TestServerCmd_AllowedHosts (t * testing.T ) {
321
+ tests := []struct {
322
+ name string
323
+ env map [string ]string
324
+ args []string
325
+ expectedErr string
326
+ expected []string // only checked if expectedErr is empty
327
+ }{
328
+ // Environment variable scenarios (space-separated format)
329
+ {
330
+ name : "env: single valid host" ,
331
+ env : map [string ]string {"AGENTAPI_ALLOWED_HOSTS" : "localhost:3284" },
332
+ args : []string {},
333
+ expected : []string {"localhost:3284" },
334
+ },
335
+ {
336
+ name : "env: multiple valid hosts space-separated" ,
337
+ env : map [string ]string {"AGENTAPI_ALLOWED_HOSTS" : "localhost:3284 example.com 192.168.1.1:8080" },
338
+ args : []string {},
339
+ expected : []string {"localhost:3284" , "example.com" , "192.168.1.1:8080" },
340
+ },
341
+ {
342
+ name : "env: host with tab" ,
343
+ env : map [string ]string {"AGENTAPI_ALLOWED_HOSTS" : "localhost:3284\t example.com" },
344
+ args : []string {},
345
+ expected : []string {"localhost:3284" , "example.com" },
346
+ },
347
+ {
348
+ name : "env: host with comma (invalid)" ,
349
+ env : map [string ]string {"AGENTAPI_ALLOWED_HOSTS" : "localhost:3284,example.com" },
350
+ args : []string {},
351
+ expectedErr : "contains comma characters" ,
352
+ },
353
+
354
+ // CLI flag scenarios (comma-separated format)
355
+ {
356
+ name : "flag: single valid host" ,
357
+ args : []string {"--allowed-hosts" , "localhost:3284" },
358
+ expected : []string {"localhost:3284" },
359
+ },
360
+ {
361
+ name : "flag: multiple valid hosts comma-separated" ,
362
+ args : []string {"--allowed-hosts" , "localhost:3284,example.com,192.168.1.1:8080" },
363
+ expected : []string {"localhost:3284" , "example.com" , "192.168.1.1:8080" },
364
+ },
365
+ {
366
+ name : "flag: multiple valid hosts with multiple flags" ,
367
+ args : []string {"--allowed-hosts" , "localhost:3284" , "--allowed-hosts" , "example.com" },
368
+ expected : []string {"localhost:3284" , "example.com" },
369
+ },
370
+ {
371
+ name : "flag: host with newline" ,
372
+ args : []string {"--allowed-hosts" , "localhost:3284\n " },
373
+ expected : []string {"localhost:3284" },
374
+ },
375
+ {
376
+ name : "flag: host with space in comma-separated list (invalid)" ,
377
+ args : []string {"--allowed-hosts" , "localhost:3284,example .com" },
378
+ expectedErr : "contains whitespace characters" ,
379
+ },
380
+
381
+ // Mixed scenarios (env + flag precedence)
382
+ {
383
+ name : "mixed: flag overrides env" ,
384
+ env : map [string ]string {"AGENTAPI_ALLOWED_HOSTS" : "localhost:8080" },
385
+ args : []string {"--allowed-hosts" , "override.com" },
386
+ expected : []string {"override.com" },
387
+ },
388
+ {
389
+ name : "mixed: flag overrides env but flag is invalid" ,
390
+ env : map [string ]string {"AGENTAPI_ALLOWED_HOSTS" : "localhost:8080" },
391
+ args : []string {"--allowed-hosts" , "invalid .com" },
392
+ expectedErr : "contains whitespace characters" ,
393
+ },
394
+
395
+ // Empty hosts are not allowed
396
+ {
397
+ name : "empty host" ,
398
+ args : []string {"--allowed-hosts" , "" },
399
+ expectedErr : "allowed hosts must not be empty" ,
400
+ },
401
+
402
+ // Default behavior
403
+ {
404
+ name : "default hosts when neither env nor flag provided" ,
405
+ args : []string {},
406
+ expected : []string {"localhost:3284" , "localhost:3000" , "localhost:3001" },
407
+ },
408
+ }
409
+
410
+ for _ , tt := range tests {
411
+ t .Run (tt .name , func (t * testing.T ) {
412
+ isolateViper (t )
413
+
414
+ // Set environment variables if provided
415
+ for key , value := range tt .env {
416
+ t .Setenv (key , value )
417
+ }
418
+
419
+ serverCmd := CreateServerCmd ()
420
+ setupCommandWithPTY (t , serverCmd )
421
+ serverCmd .SetArgs (append (tt .args , "--exit" , "dummy-command" ))
422
+ err := serverCmd .Execute ()
423
+
424
+ if tt .expectedErr != "" {
425
+ require .Error (t , err )
426
+ assert .Contains (t , err .Error (), tt .expectedErr )
427
+ } else {
428
+ require .NoError (t , err )
429
+ assert .Equal (t , tt .expected , viper .GetStringSlice (FlagAllowedHosts ))
430
+ }
431
+ })
432
+ }
433
+ }
0 commit comments