Skip to content

Commit 1c5ecb4

Browse files
radimclaude
andcommitted
test: cover profile resolution edge cases and CLI schema path helpers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d1ad7b5 commit 1c5ecb4

2 files changed

Lines changed: 239 additions & 0 deletions

File tree

crates/dry_run_cli/src/main.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,73 @@ mod tests {
12031203
assert_eq!(restored.database, "auth");
12041204
}
12051205

1206+
#[test]
1207+
fn schema_candidate_paths_explicit_first_then_profile_then_default() {
1208+
// explicit --schema-file path goes first; then resolved profile's path;
1209+
// the default-data-dir fallback is appended last
1210+
let toml = r#"
1211+
[profiles.dev]
1212+
schema_file = "from-profile.json"
1213+
"#;
1214+
let config = ProjectConfig::parse(toml).unwrap();
1215+
let explicit = PathBuf::from("/tmp/explicit.json");
1216+
let candidates = schema_candidate_paths(Some(&explicit), Some(&config), Some("dev"));
1217+
assert!(candidates.len() >= 2);
1218+
assert_eq!(candidates[0], explicit);
1219+
// second candidate is the resolved profile path (relative to cwd)
1220+
let cwd = std::env::current_dir().unwrap_or_default();
1221+
assert_eq!(candidates[1], cwd.join("from-profile.json"));
1222+
}
1223+
1224+
#[test]
1225+
fn schema_candidate_paths_no_inputs_still_includes_default_dir() {
1226+
let candidates = schema_candidate_paths(None, None, None);
1227+
// expect at least the default data-dir fallback
1228+
assert!(!candidates.is_empty());
1229+
assert!(candidates.last().unwrap().ends_with(".dryrun/schema.json"));
1230+
}
1231+
1232+
#[test]
1233+
fn resolve_schema_path_picks_first_existing() {
1234+
let dir = TempDir::new().unwrap();
1235+
let missing = dir.path().join("missing.json");
1236+
let present = dir.path().join("present.json");
1237+
std::fs::write(&present, "{}").unwrap();
1238+
1239+
// explicit path that doesn't exist; profile-resolved path that does
1240+
let toml = format!("[profiles.dev]\nschema_file = \"{}\"\n", present.display());
1241+
let config = ProjectConfig::parse(&toml).unwrap();
1242+
let resolved = resolve_schema_path(Some(&missing), Some(&config), Some("dev")).unwrap();
1243+
assert_eq!(resolved, present);
1244+
}
1245+
1246+
#[test]
1247+
fn resolve_schema_path_errors_when_nothing_exists() {
1248+
let dir = TempDir::new().unwrap();
1249+
let missing = dir.path().join("nope.json");
1250+
let result = resolve_schema_path(Some(&missing), None, None);
1251+
assert!(result.is_err());
1252+
}
1253+
1254+
#[test]
1255+
fn load_schema_file_round_trips() {
1256+
let dir = TempDir::new().unwrap();
1257+
let snap = make_snap("h1", "auth");
1258+
let path = dir.path().join("schema.json");
1259+
std::fs::write(&path, serde_json::to_string(&snap).unwrap()).unwrap();
1260+
let restored = load_schema_file(&path).unwrap();
1261+
assert_eq!(restored.content_hash, "h1");
1262+
assert_eq!(restored.database, "auth");
1263+
}
1264+
1265+
#[test]
1266+
fn load_schema_file_errors_on_invalid_json() {
1267+
let dir = TempDir::new().unwrap();
1268+
let path = dir.path().join("broken.json");
1269+
std::fs::write(&path, "{not json").unwrap();
1270+
assert!(load_schema_file(&path).is_err());
1271+
}
1272+
12061273
#[test]
12071274
fn write_snapshot_export_isolates_streams() {
12081275
let dir = TempDir::new().unwrap();

crates/dry_run_core/src/config.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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]\nschema_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

Comments
 (0)