@@ -330,3 +330,246 @@ func TestExecWithShims(t *testing.T) {
330330 got := strings .TrimSpace (output .String ())
331331 assert .Contains (t , got , "go1.21.5" , "Expected output to contain version info %v" , got )
332332}
333+
334+ // TestFindEnvVar tests the case-insensitive environment variable finder
335+ func TestFindEnvVar (t * testing.T ) {
336+ tests := []struct {
337+ name string
338+ env []string
339+ key string
340+ expectedIdx int
341+ expectedKey string
342+ onlyOnWindows bool
343+ }{
344+ {
345+ name : "exact match - PATH" ,
346+ env : []string {"HOME=/home/user" , "PATH=/usr/bin:/bin" , "USER=testuser" },
347+ key : "PATH" ,
348+ expectedIdx : 1 ,
349+ expectedKey : "PATH" ,
350+ },
351+ {
352+ name : "exact match - HOME" ,
353+ env : []string {"HOME=/home/user" , "PATH=/usr/bin" , "USER=testuser" },
354+ key : "HOME" ,
355+ expectedIdx : 0 ,
356+ expectedKey : "HOME" ,
357+ },
358+ {
359+ name : "not found" ,
360+ env : []string {"HOME=/home/user" , "USER=testuser" },
361+ key : "PATH" ,
362+ expectedIdx : - 1 ,
363+ expectedKey : "" ,
364+ },
365+ {
366+ name : "case-insensitive match - Path (Windows only)" ,
367+ env : []string {"Path=C:\\ Windows\\ System32" , "HOME=/home/user" },
368+ key : "PATH" ,
369+ expectedIdx : 0 ,
370+ expectedKey : "Path" ,
371+ onlyOnWindows : true ,
372+ },
373+ {
374+ name : "case-insensitive match - path (Windows only)" ,
375+ env : []string {"path=C:\\ Windows\\ System32" , "HOME=/home/user" },
376+ key : "PATH" ,
377+ expectedIdx : 0 ,
378+ expectedKey : "path" ,
379+ onlyOnWindows : true ,
380+ },
381+ {
382+ name : "case-insensitive match - PaTh (Windows only)" ,
383+ env : []string {"HOME=/home/user" , "PaTh=C:\\ Windows\\ System32" },
384+ key : "PATH" ,
385+ expectedIdx : 1 ,
386+ expectedKey : "PaTh" ,
387+ onlyOnWindows : true ,
388+ },
389+ }
390+
391+ for _ , tt := range tests {
392+ t .Run (tt .name , func (t * testing.T ) {
393+ if tt .onlyOnWindows && ! utils .IsWindows () {
394+ t .Skip ("Windows-only test" )
395+ }
396+
397+ idx , key := findEnvVar (tt .env , tt .key )
398+ assert .Equal (t , tt .expectedIdx , idx , "Index mismatch" )
399+ assert .Equal (t , tt .expectedKey , key , "Key mismatch" )
400+ })
401+ }
402+ }
403+
404+ // TestSetEnvVar tests setting environment variables with case-insensitive handling on Windows
405+ func TestSetEnvVar (t * testing.T ) {
406+ tests := []struct {
407+ name string
408+ env []string
409+ key string
410+ value string
411+ expected []string
412+ onlyOnWindows bool
413+ }{
414+ {
415+ name : "add new variable" ,
416+ env : []string {"HOME=/home/user" , "USER=testuser" },
417+ key : "PATH" ,
418+ value : "/usr/bin:/bin" ,
419+ expected : []string {"HOME=/home/user" , "USER=testuser" , "PATH=/usr/bin:/bin" },
420+ },
421+ {
422+ name : "update existing variable - exact match" ,
423+ env : []string {"HOME=/home/user" , "PATH=/usr/bin" , "USER=testuser" },
424+ key : "PATH" ,
425+ value : "/new/path:/usr/bin" ,
426+ expected : []string {"HOME=/home/user" , "PATH=/new/path:/usr/bin" , "USER=testuser" },
427+ },
428+ {
429+ name : "update existing variable - case mismatch on Windows" ,
430+ env : []string {"Path=C:\\ Windows\\ System32" , "HOME=/home/user" },
431+ key : "PATH" ,
432+ value : "C:\\ goenv\\ bin;C:\\ Windows\\ System32" ,
433+ expected : []string {"Path=C:\\ goenv\\ bin;C:\\ Windows\\ System32" , "HOME=/home/user" },
434+ onlyOnWindows : true ,
435+ },
436+ {
437+ name : "update existing variable - mixed case (Windows)" ,
438+ env : []string {"HOME=/home/user" , "PaTh=C:\\ Windows\\ System32" },
439+ key : "PATH" ,
440+ value : "C:\\ goenv\\ bin;C:\\ Windows\\ System32" ,
441+ expected : []string {"HOME=/home/user" , "PaTh=C:\\ goenv\\ bin;C:\\ Windows\\ System32" },
442+ onlyOnWindows : true ,
443+ },
444+ }
445+
446+ for _ , tt := range tests {
447+ t .Run (tt .name , func (t * testing.T ) {
448+ if tt .onlyOnWindows && ! utils .IsWindows () {
449+ t .Skip ("Windows-only test" )
450+ }
451+
452+ result := setEnvVar (tt .env , tt .key , tt .value )
453+ assert .Equal (t , tt .expected , result )
454+ })
455+ }
456+ }
457+
458+ // TestPrependToPath tests prepending directories to PATH with case-insensitive handling on Windows
459+ func TestPrependToPath (t * testing.T ) {
460+ tests := []struct {
461+ name string
462+ env []string
463+ dir string
464+ expected []string
465+ onlyOnWindows bool
466+ }{
467+ {
468+ name : "prepend to existing PATH - Unix" ,
469+ env : []string {"HOME=/home/user" , "PATH=/usr/bin:/bin" , "USER=testuser" },
470+ dir : "/opt/goenv/bin" ,
471+ expected : []string {"HOME=/home/user" , "PATH=/opt/goenv/bin:/usr/bin:/bin" , "USER=testuser" },
472+ },
473+ {
474+ name : "add PATH when missing" ,
475+ env : []string {"HOME=/home/user" , "USER=testuser" },
476+ dir : "/opt/goenv/bin" ,
477+ expected : []string {"HOME=/home/user" , "USER=testuser" , "PATH=/opt/goenv/bin" },
478+ },
479+ {
480+ name : "prepend to existing Path - Windows (case mismatch)" ,
481+ env : []string {"Path=C:\\ Windows\\ System32;C:\\ Windows" , "HOME=C:\\ Users\\ test" },
482+ dir : "C:\\ goenv\\ bin" ,
483+ expected : []string {"Path=C:\\ goenv\\ bin;C:\\ Windows\\ System32;C:\\ Windows" , "HOME=C:\\ Users\\ test" },
484+ onlyOnWindows : true ,
485+ },
486+ {
487+ name : "prepend to existing path - Windows (lowercase)" ,
488+ env : []string {"HOME=C:\\ Users\\ test" , "path=C:\\ Windows\\ System32" },
489+ dir : "C:\\ goenv\\ bin" ,
490+ expected : []string {"HOME=C:\\ Users\\ test" , "path=C:\\ goenv\\ bin;C:\\ Windows\\ System32" },
491+ onlyOnWindows : true ,
492+ },
493+ {
494+ name : "prepend to existing PaTh - Windows (mixed case)" ,
495+ env : []string {"PaTh=C:\\ Windows\\ System32" , "USER=testuser" },
496+ dir : "C:\\ goenv\\ bin" ,
497+ expected : []string {"PaTh=C:\\ goenv\\ bin;C:\\ Windows\\ System32" , "USER=testuser" },
498+ onlyOnWindows : true ,
499+ },
500+ }
501+
502+ for _ , tt := range tests {
503+ t .Run (tt .name , func (t * testing.T ) {
504+ if tt .onlyOnWindows && ! utils .IsWindows () {
505+ t .Skip ("Windows-only test" )
506+ }
507+
508+ result := prependToPath (tt .env , tt .dir )
509+ assert .Equal (t , tt .expected , result )
510+ })
511+ }
512+ }
513+
514+ // TestWindowsPathPreservation is a regression test for issue #557
515+ // It verifies that the original system PATH is preserved on Windows when goenv exec
516+ // prepends its own paths, even when PATH has different casing (Path, path, PATH, etc.)
517+ func TestWindowsPathPreservation (t * testing.T ) {
518+ if ! utils .IsWindows () {
519+ t .Skip ("Windows-only test" )
520+ }
521+
522+ tests := []struct {
523+ name string
524+ env []string
525+ dir string
526+ checkFor string // a path component that should still be present after prepending
527+ }{
528+ {
529+ name : "Path with C:\\ TDM-GCC-64\\ bin (issue #557 scenario)" ,
530+ env : []string {"Path=C:\\ Windows\\ System32;C:\\ TDM-GCC-64\\ bin;C:\\ Windows" },
531+ dir : "C:\\ Users\\ test\\ .goenv\\ versions\\ 1.24.0\\ bin" ,
532+ checkFor : "C:\\ TDM-GCC-64\\ bin" ,
533+ },
534+ {
535+ name : "PATH with system paths" ,
536+ env : []string {"PATH=C:\\ Windows\\ System32;C:\\ Program Files\\ Git\\ cmd" },
537+ dir : "C:\\ goenv\\ bin" ,
538+ checkFor : "C:\\ Program Files\\ Git\\ cmd" ,
539+ },
540+ {
541+ name : "path (lowercase) with MinGW" ,
542+ env : []string {"path=C:\\ msys64\\ mingw64\\ bin;C:\\ Windows\\ System32" },
543+ dir : "C:\\ goenv\\ bin" ,
544+ checkFor : "C:\\ msys64\\ mingw64\\ bin" ,
545+ },
546+ }
547+
548+ for _ , tt := range tests {
549+ t .Run (tt .name , func (t * testing.T ) {
550+ result := prependToPath (tt .env , tt .dir )
551+
552+ // Find the PATH variable in the result (case-insensitive)
553+ var pathValue string
554+ for _ , envVar := range result {
555+ if idx := strings .Index (envVar , "=" ); idx != - 1 {
556+ key := envVar [:idx ]
557+ if strings .EqualFold (key , "PATH" ) {
558+ pathValue = envVar [idx + 1 :]
559+ break
560+ }
561+ }
562+ }
563+
564+ require .NotEmpty (t , pathValue , "PATH should be present in result" )
565+
566+ // Verify the new directory was prepended
567+ assert .True (t , strings .HasPrefix (pathValue , tt .dir + ";" ),
568+ "PATH should start with new directory: got %s" , pathValue )
569+
570+ // Verify the original path component is still present (regression test for #557)
571+ assert .Contains (t , pathValue , tt .checkFor ,
572+ "Original path component %s should be preserved in PATH: got %s" , tt .checkFor , pathValue )
573+ })
574+ }
575+ }
0 commit comments