@@ -698,4 +698,176 @@ database_id = "stg"
698698 // root path has no file_name; falls back to "default"
699699 assert_eq ! ( config. project_id( Path :: new( "/" ) ) . 0 , "default" ) ;
700700 }
701+
702+ #[ test]
703+ fn explicit_profile_overrides_default_profile ( ) {
704+ let toml = r#"
705+ [default]
706+ profile = "prod"
707+
708+ [profiles.prod]
709+ schema_file = "prod.json"
710+
711+ [profiles.dev]
712+ schema_file = "dev.json"
713+ "# ;
714+ let config = ProjectConfig :: parse ( toml) . unwrap ( ) ;
715+ let resolved = config
716+ . resolve_profile ( None , None , Some ( "dev" ) , Path :: new ( "/p" ) )
717+ . unwrap ( ) ;
718+ assert_eq ! ( resolved. name, "dev" ) ;
719+ assert_eq ! ( resolved. schema_file. unwrap( ) , PathBuf :: from( "/p/dev.json" ) ) ;
720+ }
721+
722+ #[ test]
723+ fn resolve_profile_absolute_schema_path_kept_as_is ( ) {
724+ let toml = r#"
725+ [profiles.dev]
726+ schema_file = "/abs/schema.json"
727+ "# ;
728+ let config = ProjectConfig :: parse ( toml) . unwrap ( ) ;
729+ let resolved = config
730+ . resolve_profile ( None , None , Some ( "dev" ) , Path :: new ( "/project" ) )
731+ . unwrap ( ) ;
732+ assert_eq ! (
733+ resolved. schema_file. unwrap( ) ,
734+ PathBuf :: from( "/abs/schema.json" )
735+ ) ;
736+ }
737+
738+ #[ test]
739+ fn resolve_profile_empty_database_id_falls_back_to_profile_name ( ) {
740+ let toml = r#"
741+ [profiles.staging]
742+ schema_file = "x.json"
743+ database_id = ""
744+ "# ;
745+ let config = ProjectConfig :: parse ( toml) . unwrap ( ) ;
746+ let resolved = config
747+ . resolve_profile ( None , None , Some ( "staging" ) , Path :: new ( "/p" ) )
748+ . unwrap ( ) ;
749+ assert_eq ! (
750+ resolved. database_id. as_ref( ) . map( |d| d. 0 . as_str( ) ) ,
751+ Some ( "staging" )
752+ ) ;
753+ }
754+
755+ #[ test]
756+ fn resolve_profile_auto_discovers_schema_json ( ) {
757+ let dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
758+ let dryrun_dir = dir. path ( ) . join ( ".dryrun" ) ;
759+ std:: fs:: create_dir_all ( & dryrun_dir) . unwrap ( ) ;
760+ std:: fs:: write ( dryrun_dir. join ( "schema.json" ) , "{}" ) . unwrap ( ) ;
761+
762+ let config = ProjectConfig :: parse ( "" ) . unwrap ( ) ;
763+ let resolved = config
764+ . resolve_profile ( None , None , None , dir. path ( ) )
765+ . unwrap ( ) ;
766+ assert_eq ! ( resolved. name, "<auto>" ) ;
767+ assert ! ( resolved. database_id. is_none( ) ) ;
768+ assert_eq ! (
769+ resolved. schema_file. unwrap( ) ,
770+ dir. path( ) . join( ".dryrun/schema.json" )
771+ ) ;
772+ }
773+
774+ #[ test]
775+ fn resolve_profile_cli_schema_without_profile_falls_back ( ) {
776+ let config = ProjectConfig :: parse ( "" ) . unwrap ( ) ;
777+ let p = PathBuf :: from ( "/some/where.json" ) ;
778+ let resolved = config
779+ . resolve_profile ( None , Some ( & p) , None , Path :: new ( "/p" ) )
780+ . unwrap ( ) ;
781+ assert_eq ! ( resolved. name, "<cli>" ) ;
782+ assert_eq ! ( resolved. schema_file. as_deref( ) , Some ( p. as_path( ) ) ) ;
783+ assert ! ( resolved. db_url. is_none( ) ) ;
784+ }
785+
786+ #[ test]
787+ fn resolve_profile_no_profile_no_schema_no_cli_errors ( ) {
788+ let dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
789+ let config = ProjectConfig :: parse ( "" ) . unwrap ( ) ;
790+ let result = config. resolve_profile ( None , None , None , dir. path ( ) ) ;
791+ assert ! ( result. is_err( ) ) ;
792+ }
793+
794+ #[ test]
795+ fn expand_env_vars_multiple_in_one_string ( ) {
796+ // SAFETY: test-only, single-threaded test runner
797+ unsafe {
798+ std:: env:: set_var ( "DRYRUN_A" , "alpha" ) ;
799+ std:: env:: set_var ( "DRYRUN_B" , "beta" ) ;
800+ }
801+ assert_eq ! ( expand_env_vars( "${DRYRUN_A}-${DRYRUN_B}" ) , "alpha-beta" ) ;
802+ unsafe {
803+ std:: env:: remove_var ( "DRYRUN_A" ) ;
804+ std:: env:: remove_var ( "DRYRUN_B" ) ;
805+ }
806+ }
807+
808+ #[ test]
809+ fn expand_env_vars_unterminated_brace_left_alone ( ) {
810+ // no closing brace — should not loop forever, return as-is
811+ assert_eq ! ( expand_env_vars( "foo ${UNCLOSED bar" ) , "foo ${UNCLOSED bar" ) ;
812+ }
813+
814+ #[ test]
815+ fn discover_finds_config_in_parent ( ) {
816+ let dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
817+ // simulate repo root
818+ std:: fs:: create_dir ( dir. path ( ) . join ( ".git" ) ) . unwrap ( ) ;
819+ std:: fs:: write (
820+ dir. path ( ) . join ( "dryrun.toml" ) ,
821+ "[profiles.dev]\n schema_file = \" x.json\" \n " ,
822+ )
823+ . unwrap ( ) ;
824+
825+ let nested = dir. path ( ) . join ( "a" ) . join ( "b" ) ;
826+ std:: fs:: create_dir_all ( & nested) . unwrap ( ) ;
827+ let ( path, config) = ProjectConfig :: discover ( & nested) . unwrap ( ) ;
828+ assert_eq ! ( path, dir. path( ) . join( "dryrun.toml" ) ) ;
829+ assert ! ( config. profiles. contains_key( "dev" ) ) ;
830+ }
831+
832+ #[ test]
833+ fn discover_stops_at_git_root ( ) {
834+ let dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
835+ // .git in inner dir, dryrun.toml only above it — discovery must NOT cross the boundary
836+ std:: fs:: create_dir ( dir. path ( ) . join ( ".git" ) ) . unwrap ( ) ;
837+ std:: fs:: write (
838+ dir. path ( ) . parent ( ) . unwrap ( ) . join ( "dryrun.toml" ) ,
839+ "[profiles.dev]\n " ,
840+ )
841+ . ok ( ) ;
842+ // discovery from the git root should not find the parent's dryrun.toml
843+ assert ! ( ProjectConfig :: discover( dir. path( ) ) . is_none( ) ) ;
844+ }
845+
846+ #[ test]
847+ fn pgmustard_api_key_from_config_expands_env ( ) {
848+ // SAFETY: test-only, single-threaded test runner
849+ unsafe { std:: env:: set_var ( "DRYRUN_PGM_KEY" , "sk-test-123" ) } ;
850+ let toml = r#"
851+ [services]
852+ pgmustard_api_key = "${DRYRUN_PGM_KEY}"
853+ "# ;
854+ let config = ProjectConfig :: parse ( toml) . unwrap ( ) ;
855+ assert_eq ! ( config. pgmustard_api_key( ) . as_deref( ) , Some ( "sk-test-123" ) ) ;
856+ unsafe { std:: env:: remove_var ( "DRYRUN_PGM_KEY" ) } ;
857+ }
858+
859+ #[ test]
860+ fn pgmustard_api_key_empty_after_expansion_falls_through ( ) {
861+ // SAFETY: test-only, single-threaded test runner
862+ unsafe {
863+ std:: env:: remove_var ( "DRYRUN_PGM_MISSING" ) ;
864+ std:: env:: remove_var ( "PGMUSTARD_API_KEY" ) ;
865+ }
866+ let toml = r#"
867+ [services]
868+ pgmustard_api_key = "${DRYRUN_PGM_MISSING}"
869+ "# ;
870+ let config = ProjectConfig :: parse ( toml) . unwrap ( ) ;
871+ assert ! ( config. pgmustard_api_key( ) . is_none( ) ) ;
872+ }
701873}
0 commit comments