@@ -119,6 +119,150 @@ func TestValidateContainerID_SecurityCritical(t *testing.T) {
119119 }
120120}
121121
122+ // TestExpandEnvArgs tests ExpandEnvArgs with various -e flag combinations.
123+ func TestExpandEnvArgs (t * testing.T ) {
124+ tests := []struct {
125+ name string
126+ args []string
127+ envVars map [string ]string
128+ want []string
129+ }{
130+ {
131+ name : "nil input returns empty slice" ,
132+ args : nil ,
133+ want : []string {},
134+ },
135+ {
136+ name : "empty input returns empty slice" ,
137+ args : []string {},
138+ want : []string {},
139+ },
140+ {
141+ name : "args without -e flag pass through unchanged" ,
142+ args : []string {"run" , "--rm" , "-i" , "ghcr.io/org/image:latest" },
143+ want : []string {"run" , "--rm" , "-i" , "ghcr.io/org/image:latest" },
144+ },
145+ {
146+ name : "-e VAR_NAME is expanded when env var is set" ,
147+ args : []string {"-e" , "MY_TOKEN" },
148+ envVars : map [string ]string {"MY_TOKEN" : "secret-value" },
149+ want : []string {"-e" , "MY_TOKEN=secret-value" },
150+ },
151+ {
152+ name : "-e VAR_NAME is left unchanged when env var is not set" ,
153+ args : []string {"-e" , "_DOCKERENV_TEST_TRULY_UNSET_XYZ999" },
154+ want : []string {"-e" , "_DOCKERENV_TEST_TRULY_UNSET_XYZ999" },
155+ },
156+ {
157+ name : "-e VAR=VALUE (already has =) is passed through unchanged" ,
158+ args : []string {"-e" , "MY_VAR=already-set" },
159+ envVars : map [string ]string {"MY_VAR" : "other-value" },
160+ want : []string {"-e" , "MY_VAR=already-set" },
161+ },
162+ {
163+ name : "-e at end of args (no following value) is passed through" ,
164+ args : []string {"run" , "-e" },
165+ want : []string {"run" , "-e" },
166+ },
167+ {
168+ name : "multiple -e flags are all expanded" ,
169+ args : []string {"-e" , "TOKEN_A" , "-e" , "TOKEN_B" },
170+ envVars : map [string ]string {"TOKEN_A" : "val-a" , "TOKEN_B" : "val-b" },
171+ want : []string {"-e" , "TOKEN_A=val-a" , "-e" , "TOKEN_B=val-b" },
172+ },
173+ {
174+ name : "mix of set and unset env vars in -e flags" ,
175+ args : []string {"-e" , "SET_VAR" , "-e" , "_DOCKERENV_TEST_TRULY_UNSET_XYZ999" },
176+ envVars : map [string ]string {"SET_VAR" : "value" },
177+ want : []string {"-e" , "SET_VAR=value" , "-e" , "_DOCKERENV_TEST_TRULY_UNSET_XYZ999" },
178+ },
179+ {
180+ name : "realistic docker run command with env var expansion" ,
181+ args : []string {"run" , "--rm" , "-e" , "GITHUB_PERSONAL_ACCESS_TOKEN" , "-i" , "ghcr.io/github/github-mcp-server:latest" },
182+ envVars : map [string ]string {"GITHUB_PERSONAL_ACCESS_TOKEN" : "ghp_abc123" },
183+ want : []string {"run" , "--rm" , "-e" , "GITHUB_PERSONAL_ACCESS_TOKEN=ghp_abc123" , "-i" , "ghcr.io/github/github-mcp-server:latest" },
184+ },
185+ {
186+ name : "-e VAR where VAR is empty string is passed through unchanged" ,
187+ args : []string {"-e" , "" },
188+ want : []string {"-e" , "" },
189+ },
190+ {
191+ name : "env var with empty value is expanded with empty value" ,
192+ args : []string {"-e" , "EMPTY_VAR" },
193+ envVars : map [string ]string {"EMPTY_VAR" : "" },
194+ want : []string {"-e" , "EMPTY_VAR=" },
195+ },
196+ {
197+ name : "env var value containing = is expanded correctly" ,
198+ args : []string {"-e" , "URL_VAR" },
199+ envVars : map [string ]string {"URL_VAR" : "https://example.com?key=val" },
200+ want : []string {"-e" , "URL_VAR=https://example.com?key=val" },
201+ },
202+ {
203+ name : "non -e flags between expanded flags are preserved" ,
204+ args : []string {"-e" , "VAR_A" , "--name" , "mycontainer" , "-e" , "VAR_B" },
205+ envVars : map [string ]string {"VAR_A" : "alpha" , "VAR_B" : "beta" },
206+ want : []string {"-e" , "VAR_A=alpha" , "--name" , "mycontainer" , "-e" , "VAR_B=beta" },
207+ },
208+ {
209+ name : "same var referenced multiple times is expanded each time" ,
210+ args : []string {"-e" , "SHARED_VAR" , "-e" , "SHARED_VAR" },
211+ envVars : map [string ]string {"SHARED_VAR" : "shared" },
212+ want : []string {"-e" , "SHARED_VAR=shared" , "-e" , "SHARED_VAR=shared" },
213+ },
214+ {
215+ name : "-e immediately followed by another -e flag where first -e var is unset" ,
216+ args : []string {"-e" , "-e" },
217+ // The first "-e" tries to expand the second "-e" as a var name.
218+ // Since no env var named "-e" is set, the expansion is skipped.
219+ // Both "-e" args are emitted as-is.
220+ want : []string {"-e" , "-e" },
221+ },
222+ }
223+
224+ for _ , tt := range tests {
225+ t .Run (tt .name , func (t * testing.T ) {
226+ for k , v := range tt .envVars {
227+ t .Setenv (k , v )
228+ }
229+
230+ got := ExpandEnvArgs (tt .args )
231+
232+ require .NotNil (t , got , "ExpandEnvArgs should never return nil" )
233+ assert .Equal (t , tt .want , got )
234+ })
235+ }
236+ }
237+
238+ // TestExpandEnvArgs_DoesNotMutateInput verifies that the original args slice
239+ // is not modified by ExpandEnvArgs.
240+ func TestExpandEnvArgs_DoesNotMutateInput (t * testing.T ) {
241+ t .Setenv ("MY_SECRET" , "secret-value" )
242+
243+ original := []string {"-e" , "MY_SECRET" , "--rm" }
244+ // Make a copy to compare against after the call
245+ copyOfOriginal := make ([]string , len (original ))
246+ copy (copyOfOriginal , original )
247+
248+ ExpandEnvArgs (original )
249+
250+ assert .Equal (t , copyOfOriginal , original , "ExpandEnvArgs must not mutate the input slice" )
251+ }
252+
253+ // TestExpandEnvArgs_OutputIsIndependentOfInput verifies that modifications to
254+ // the returned slice do not affect the original input.
255+ func TestExpandEnvArgs_OutputIsIndependentOfInput (t * testing.T ) {
256+ t .Setenv ("SOME_VAR" , "value" )
257+
258+ args := []string {"run" , "-e" , "SOME_VAR" }
259+ result := ExpandEnvArgs (args )
260+
261+ // Modifying the result should not affect the original
262+ result [0 ] = "MODIFIED"
263+ assert .Equal (t , "run" , args [0 ], "Modifying result should not affect original slice" )
264+ }
265+
122266// TestGetGatewayPortFromEnv tests the env-based gateway port parsing.
123267func TestGetGatewayPortFromEnv_Comprehensive (t * testing.T ) {
124268 tests := []struct {
0 commit comments