@@ -623,6 +623,168 @@ class CliMainTest < CliTestCase
623623 end
624624 end
625625
626+ test "external command from local .kamal/bin" do
627+ Dir . mktmpdir do |tmpdir |
628+ Dir . chdir ( tmpdir ) do
629+ FileUtils . mkdir_p ( ".kamal/bin" )
630+ File . write ( ".kamal/bin/foo" , "#!/bin/sh\n " )
631+ File . chmod ( 0755 , ".kamal/bin/foo" )
632+
633+ Kamal ::Cli ::Main . expects ( :exec ) . with ( ".kamal/bin/foo" )
634+ with_argv ( [ "foo" ] ) { Kamal ::Cli ::Main . start }
635+ end
636+ end
637+ end
638+
639+ test "external command from PATH" do
640+ Dir . mktmpdir do |tmpdir |
641+ Dir . chdir ( tmpdir ) do
642+ bin_dir = File . join ( tmpdir , "bin" )
643+ FileUtils . mkdir_p ( bin_dir )
644+ File . write ( File . join ( bin_dir , "kamal-bar" ) , "#!/bin/sh\n " )
645+ File . chmod ( 0755 , File . join ( bin_dir , "kamal-bar" ) )
646+
647+ original_path = ENV [ "PATH" ]
648+ ENV [ "PATH" ] = "#{ bin_dir } #{ File ::PATH_SEPARATOR } #{ original_path } "
649+
650+ Kamal ::Cli ::Main . expects ( :exec ) . with ( File . join ( bin_dir , "kamal-bar" ) )
651+ with_argv ( [ "bar" ] ) { Kamal ::Cli ::Main . start }
652+ ensure
653+ ENV [ "PATH" ] = original_path
654+ end
655+ end
656+ end
657+
658+ test "local external command takes priority over PATH" do
659+ Dir . mktmpdir do |tmpdir |
660+ Dir . chdir ( tmpdir ) do
661+ FileUtils . mkdir_p ( ".kamal/bin" )
662+ File . write ( ".kamal/bin/baz" , "#!/bin/sh\n " )
663+ File . chmod ( 0755 , ".kamal/bin/baz" )
664+
665+ bin_dir = File . join ( tmpdir , "path_bin" )
666+ FileUtils . mkdir_p ( bin_dir )
667+ File . write ( File . join ( bin_dir , "kamal-baz" ) , "#!/bin/sh\n " )
668+ File . chmod ( 0755 , File . join ( bin_dir , "kamal-baz" ) )
669+
670+ original_path = ENV [ "PATH" ]
671+ ENV [ "PATH" ] = "#{ bin_dir } #{ File ::PATH_SEPARATOR } #{ original_path } "
672+
673+ Kamal ::Cli ::Main . expects ( :exec ) . with ( ".kamal/bin/baz" )
674+ with_argv ( [ "baz" ] ) { Kamal ::Cli ::Main . start }
675+ ensure
676+ ENV [ "PATH" ] = original_path
677+ end
678+ end
679+ end
680+
681+ test "builtin commands are not shadowed by external commands" do
682+ Dir . mktmpdir do |tmpdir |
683+ Dir . chdir ( tmpdir ) do
684+ FileUtils . mkdir_p ( ".kamal/bin" )
685+ File . write ( ".kamal/bin/version" , "#!/bin/sh\n " )
686+ File . chmod ( 0755 , ".kamal/bin/version" )
687+
688+ Kamal ::Cli ::Main . expects ( :exec ) . never
689+ with_argv ( [ "version" ] ) { Kamal ::Cli ::Main . start }
690+ end
691+ end
692+ end
693+
694+ test "builtin prefix matches are not shadowed by external commands" do
695+ Dir . mktmpdir do |tmpdir |
696+ Dir . chdir ( tmpdir ) do
697+ FileUtils . mkdir_p ( ".kamal/bin" )
698+ File . write ( ".kamal/bin/ver" , "#!/bin/sh\n " )
699+ File . chmod ( 0755 , ".kamal/bin/ver" )
700+
701+ Kamal ::Cli ::Main . expects ( :exec ) . never
702+ with_argv ( [ "ver" ] ) { Kamal ::Cli ::Main . start }
703+ end
704+ end
705+ end
706+
707+ test "external command passes arguments through" do
708+ Dir . mktmpdir do |tmpdir |
709+ Dir . chdir ( tmpdir ) do
710+ FileUtils . mkdir_p ( ".kamal/bin" )
711+ File . write ( ".kamal/bin/foo" , "#!/bin/sh\n " )
712+ File . chmod ( 0755 , ".kamal/bin/foo" )
713+
714+ Kamal ::Cli ::Main . expects ( :exec ) . with ( ".kamal/bin/foo" , "--bar" , "baz" )
715+ with_argv ( [ "foo" , "--bar" , "baz" ] ) { Kamal ::Cli ::Main . start }
716+ end
717+ end
718+ end
719+
720+ test "non-executable file in .kamal/bin is skipped" do
721+ Dir . mktmpdir do |tmpdir |
722+ Dir . chdir ( tmpdir ) do
723+ FileUtils . mkdir_p ( ".kamal/bin" )
724+ File . write ( ".kamal/bin/foo" , "#!/bin/sh\n " )
725+ File . chmod ( 0644 , ".kamal/bin/foo" )
726+
727+ Kamal ::Cli ::Main . expects ( :exec ) . never
728+ with_argv ( [ "foo" ] ) do
729+ error = assert_raises ( RuntimeError ) { Kamal ::Cli ::Main . start }
730+ assert_match /Configuration file not found/ , error . message
731+ end
732+ end
733+ end
734+ end
735+
736+ test "directory in .kamal/bin is skipped" do
737+ Dir . mktmpdir do |tmpdir |
738+ Dir . chdir ( tmpdir ) do
739+ FileUtils . mkdir_p ( ".kamal/bin/foo" )
740+
741+ Kamal ::Cli ::Main . expects ( :exec ) . never
742+ with_argv ( [ "foo" ] ) do
743+ error = assert_raises ( RuntimeError ) { Kamal ::Cli ::Main . start }
744+ assert_match /Configuration file not found/ , error . message
745+ end
746+ end
747+ end
748+ end
749+
750+ test "directory on PATH is skipped" do
751+ Dir . mktmpdir do |tmpdir |
752+ Dir . chdir ( tmpdir ) do
753+ bin_dir = File . join ( tmpdir , "bin" )
754+ FileUtils . mkdir_p ( File . join ( bin_dir , "kamal-foo" ) )
755+
756+ original_path = ENV [ "PATH" ]
757+ ENV [ "PATH" ] = "#{ bin_dir } #{ File ::PATH_SEPARATOR } #{ original_path } "
758+
759+ Kamal ::Cli ::Main . expects ( :exec ) . never
760+ with_argv ( [ "foo" ] ) do
761+ error = assert_raises ( RuntimeError ) { Kamal ::Cli ::Main . start }
762+ assert_match /Configuration file not found/ , error . message
763+ end
764+ ensure
765+ ENV [ "PATH" ] = original_path
766+ end
767+ end
768+ end
769+
770+ test "path traversal in command name is rejected" do
771+ Dir . mktmpdir do |tmpdir |
772+ Dir . chdir ( tmpdir ) do
773+ FileUtils . mkdir_p ( ".kamal/bin" )
774+ target = File . join ( tmpdir , "evil" )
775+ File . write ( target , "#!/bin/sh\n " )
776+ File . chmod ( 0755 , target )
777+ File . symlink ( target , ".kamal/bin/../foo" )
778+
779+ Kamal ::Cli ::Main . expects ( :exec ) . never
780+ with_argv ( [ "../foo" ] ) do
781+ error = assert_raises ( RuntimeError ) { Kamal ::Cli ::Main . start }
782+ assert_match /Configuration file not found/ , error . message
783+ end
784+ end
785+ end
786+ end
787+
626788 test "upgrade rolling" do
627789 invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml" , "skip_hooks" => false , "confirmed" => true , "rolling" => false }
628790 Kamal ::Cli ::Main . any_instance . expects ( :invoke ) . with ( "kamal:cli:proxy:upgrade" , [ ] , invoke_options ) . times ( 4 )
0 commit comments