@@ -6,12 +6,38 @@ import (
66 "strings"
77 "testing"
88
9+ "github.com/ActiveState/termtest/xpty"
910 "github.com/spf13/cobra"
1011 "github.com/spf13/viper"
1112 "github.com/stretchr/testify/assert"
1213 "github.com/stretchr/testify/require"
1314)
1415
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+
1541func TestParseAgentType (t * testing.T ) {
1642 tests := []struct {
1743 firstArg string
@@ -141,17 +167,16 @@ func TestServerCmd_AllArgs_Defaults(t *testing.T) {
141167 {"chat-base-path default" , FlagChatBasePath , "/chat" , func () any { return viper .GetString (FlagChatBasePath ) }},
142168 {"term-width default" , FlagTermWidth , uint16 (80 ), func () any { return viper .GetUint16 (FlagTermWidth ) }},
143169 {"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 ) }},
144171 }
145172
146173 for _ , tt := range tests {
147174 t .Run (tt .name , func (t * testing.T ) {
148175 isolateViper (t )
149176 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" })
155180 if err := serverCmd .Execute (); err != nil {
156181 t .Fatalf ("Failed to execute server command: %v" , err )
157182 }
@@ -175,6 +200,7 @@ func TestServerCmd_AllEnvVars(t *testing.T) {
175200 {"AGENTAPI_CHAT_BASE_PATH" , "AGENTAPI_CHAT_BASE_PATH" , "/api" , "/api" , func () any { return viper .GetString (FlagChatBasePath ) }},
176201 {"AGENTAPI_TERM_WIDTH" , "AGENTAPI_TERM_WIDTH" , "120" , uint16 (120 ), func () any { return viper .GetUint16 (FlagTermWidth ) }},
177202 {"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 ) }},
178204 }
179205
180206 for _ , tt := range tests {
@@ -183,10 +209,8 @@ func TestServerCmd_AllEnvVars(t *testing.T) {
183209 t .Setenv (tt .envVar , tt .envValue )
184210
185211 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" })
190214 if err := serverCmd .Execute (); err != nil {
191215 t .Fatalf ("Failed to execute server command: %v" , err )
192216 }
@@ -254,9 +278,9 @@ func TestServerCmd_ArgsPrecedenceOverEnv(t *testing.T) {
254278 isolateViper (t )
255279 t .Setenv (tt .envVar , tt .envValue )
256280
257- // Mock execution to test arg parsing without running server
258- args := append (tt .args , "--help" )
281+ args := append (tt .args , "--exit" , "dummy-command" )
259282 serverCmd := CreateServerCmd ()
283+ setupCommandWithPTY (t , serverCmd )
260284 serverCmd .SetArgs (args )
261285 if err := serverCmd .Execute (); err != nil {
262286 t .Fatalf ("Failed to execute server command: %v" , err )
@@ -277,7 +301,8 @@ func TestMixed_ConfigurationScenarios(t *testing.T) {
277301
278302 // Set some CLI args
279303 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" })
281306 if err := serverCmd .Execute (); err != nil {
282307 t .Fatalf ("Failed to execute server command: %v" , err )
283308 }
@@ -291,3 +316,118 @@ func TestMixed_ConfigurationScenarios(t *testing.T) {
291316 assert .Equal (t , uint16 (1000 ), viper .GetUint16 (FlagTermHeight )) // default
292317 })
293318}
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