use super::*;
use crate::config::merge::Merge;
use crate::config::types::*;
#[test]
fn test_config_input_merge() {
let file_config = ConfigInput {
databases: Some(DatabasesInput {
dev_url: Some("postgres://localhost/dev".to_string()),
shadow_url: None,
target_url: None,
shadow: Some(ShadowDatabaseInput {
auto: Some(true),
reset: None,
url: None,
docker: None,
}),
}),
directories: Some(DirectoriesInput {
schema_dir: Some("schema".to_string()),
migrations_dir: None,
baselines_dir: None,
roles_file: None,
}),
objects: None,
migration: None,
schema: None,
docker: None,
};
let cli_config = ConfigInput {
databases: Some(DatabasesInput {
dev_url: None, shadow_url: Some("postgres://localhost/shadow_override".to_string()),
target_url: Some("postgres://localhost/target".to_string()),
shadow: None,
}),
directories: Some(DirectoriesInput {
schema_dir: None,
migrations_dir: Some("migrations_override".to_string()),
baselines_dir: Some("baselines".to_string()),
roles_file: Some("roles.sql".to_string()),
}),
objects: Some(ObjectsInput {
include: None,
exclude: Some(ObjectExcludeInput {
schemas: Some(vec!["temp_*".to_string()]),
tables: None,
}),
}),
migration: None,
schema: None,
docker: None,
};
let merged = file_config.merge(cli_config);
assert_eq!(
merged.databases.as_ref().unwrap().dev_url,
Some("postgres://localhost/dev".to_string())
);
assert_eq!(
merged.databases.as_ref().unwrap().shadow_url,
Some("postgres://localhost/shadow_override".to_string())
);
assert_eq!(
merged.databases.as_ref().unwrap().target_url,
Some("postgres://localhost/target".to_string())
);
assert_eq!(
merged.directories.as_ref().unwrap().schema_dir,
Some("schema".to_string())
);
assert_eq!(
merged.directories.as_ref().unwrap().migrations_dir,
Some("migrations_override".to_string())
);
assert_eq!(
merged.directories.as_ref().unwrap().baselines_dir,
Some("baselines".to_string())
);
assert!(merged.objects.is_some());
assert_eq!(
merged
.objects
.as_ref()
.unwrap()
.exclude
.as_ref()
.unwrap()
.schemas,
Some(vec!["temp_*".to_string()])
);
}
#[test]
fn test_config_builder_resolve() {
let config_input = ConfigInput {
databases: Some(DatabasesInput {
dev_url: Some("postgres://localhost/dev".to_string()),
shadow_url: None,
target_url: None,
shadow: None, }),
directories: None, objects: Some(ObjectsInput {
include: None,
exclude: Some(ObjectExcludeInput {
schemas: Some(vec!["pg_*".to_string()]),
tables: Some(vec!["temp_*".to_string()]),
}),
}),
migration: None, schema: None, docker: None, };
let dev = DevUrlArgs::default().resolve(&config_input).unwrap();
assert_eq!(dev.as_str(), "postgres://localhost/dev");
match ShadowUrlArgs::default().resolve(&config_input).unwrap() {
ShadowDatabase::Auto => {} _ => panic!("Expected auto shadow database"),
}
assert_eq!(TargetUrlArgs::default().lookup(&config_input), None);
let config = ConfigBuilder::new()
.with_file(config_input)
.resolve()
.unwrap();
assert_eq!(config.directories.schema, "schema");
assert_eq!(config.directories.migrations, "migrations");
assert_eq!(config.directories.baselines, "schema_baselines");
assert_eq!(config.directories.roles, "roles.sql");
assert_eq!(config.objects.exclude.schemas, vec!["pg_*".to_string()]);
assert_eq!(config.objects.exclude.tables, vec!["temp_*".to_string()]);
assert_eq!(config.migration.default_mode, "safe_only");
assert!(config.migration.validate_baseline_consistency);
assert!(config.docker.auto_cleanup);
assert!(config.docker.check_system_identifier);
}
#[tokio::test]
async fn test_shadow_database_url_mode() {
let shadow_db = ShadowDatabase::Url {
url: "postgres://localhost/explicit_shadow".to_string(),
reset: ShadowResetMode::default(),
};
let url = shadow_db.get_connection_string().await.unwrap();
assert_eq!(url, "postgres://localhost/explicit_shadow");
}
#[test]
fn test_object_filter_schema_filtering() {
let objects = Objects {
include: ObjectInclude {
schemas: vec!["public".to_string(), "app".to_string()],
tables: vec![],
},
exclude: ObjectExclude {
schemas: vec!["pg_*".to_string(), "information_schema".to_string()],
tables: vec![],
},
};
let tracking_table = TrackingTable::default();
let filter = ObjectFilter::new(&objects, &tracking_table);
assert!(filter.should_include_schema("public"));
assert!(filter.should_include_schema("app"));
assert!(!filter.should_include_schema("pg_catalog"));
assert!(!filter.should_include_schema("information_schema"));
assert!(!filter.should_include_schema("other"));
}
#[test]
fn test_object_filter_table_filtering() {
let objects = Objects {
include: ObjectInclude {
schemas: vec!["public".to_string()],
tables: vec!["users".to_string(), "posts".to_string()],
},
exclude: ObjectExclude {
schemas: vec!["pg_*".to_string()],
tables: vec!["temp_*".to_string()],
},
};
let tracking_table = TrackingTable::default();
let filter = ObjectFilter::new(&objects, &tracking_table);
assert!(filter.should_include_table("public", "users"));
assert!(filter.should_include_table("public", "posts"));
assert!(!filter.should_include_table("public", "temp_data"));
assert!(!filter.should_include_table("public", "other_table"));
assert!(!filter.should_include_table("pg_catalog", "pg_tables"));
}
#[test]
fn test_empty_include_patterns_means_include_all() {
let objects = Objects {
include: ObjectInclude {
schemas: vec![], tables: vec![],
},
exclude: ObjectExclude {
schemas: vec!["pg_*".to_string()],
tables: vec![],
},
};
let tracking_table = TrackingTable::default();
let filter = ObjectFilter::new(&objects, &tracking_table);
assert!(filter.should_include_schema("public"));
assert!(filter.should_include_schema("app"));
assert!(filter.should_include_schema("custom"));
assert!(!filter.should_include_schema("pg_catalog"));
assert!(!filter.should_include_schema("pg_stat"));
}
#[test]
fn test_shadow_docker_version_resolution() {
let config = ShadowDockerConfig {
version: Some("16".to_string()),
image: ShadowDockerConfig::default().image.clone(),
platform: None,
environment: Default::default(),
container_name: None,
auto_cleanup: true,
volumes: None,
network: None,
};
assert_eq!(config.resolved_image(), "postgres:16-alpine");
let config_with_both = ShadowDockerConfig {
version: Some("16".to_string()),
image: "postgres:14-bullseye".to_string(),
platform: None,
environment: Default::default(),
container_name: None,
auto_cleanup: true,
volumes: None,
network: None,
};
assert_eq!(config_with_both.resolved_image(), "postgres:14-bullseye");
let default_config = ShadowDockerConfig::default();
assert_eq!(default_config.resolved_image(), "postgres:18-alpine");
}
#[test]
fn test_config_builder_shadow_docker_version() {
let config_input = ConfigInput {
databases: Some(DatabasesInput {
dev_url: Some("postgres://localhost/dev".to_string()),
shadow_url: None,
target_url: None,
shadow: Some(ShadowDatabaseInput {
auto: None,
reset: None,
url: None,
docker: Some(ShadowDockerInput {
version: Some("16".to_string()),
image: None,
platform: None,
environment: None,
container_name: None,
auto_cleanup: None,
volumes: None,
network: None,
}),
}),
}),
directories: None,
objects: None,
migration: None,
schema: None,
docker: None,
};
match ShadowUrlArgs::default().resolve(&config_input).unwrap() {
ShadowDatabase::Docker(docker_config) => {
assert_eq!(docker_config.version, Some("16".to_string()));
assert_eq!(docker_config.resolved_image(), "postgres:16-alpine");
}
_ => panic!("Expected Docker shadow database"),
}
}
#[test]
fn test_config_builder_shadow_docker_explicit_image() {
let config_input = ConfigInput {
databases: Some(DatabasesInput {
dev_url: Some("postgres://localhost/dev".to_string()),
shadow_url: None,
target_url: None,
shadow: Some(ShadowDatabaseInput {
auto: None,
reset: None,
url: None,
docker: Some(ShadowDockerInput {
version: Some("16".to_string()), image: Some("postgres:14-bullseye".to_string()),
platform: None,
environment: None,
container_name: None,
auto_cleanup: None,
volumes: None,
network: None,
}),
}),
}),
directories: None,
objects: None,
migration: None,
schema: None,
docker: None,
};
match ShadowUrlArgs::default().resolve(&config_input).unwrap() {
ShadowDatabase::Docker(docker_config) => {
assert_eq!(docker_config.resolved_image(), "postgres:14-bullseye");
}
_ => panic!("Expected Docker shadow database"),
}
}
#[test]
fn test_default_shadow_docker_version() {
let default_config = ShadowDockerConfig::default();
assert_eq!(default_config.image, "postgres:18-alpine");
assert_eq!(default_config.resolved_image(), "postgres:18-alpine");
assert_eq!(default_config.platform, None);
}
#[test]
fn test_config_builder_shadow_docker_platform() {
let config_input = ConfigInput {
databases: Some(DatabasesInput {
dev_url: Some("postgres://localhost/dev".to_string()),
shadow_url: None,
target_url: None,
shadow: Some(ShadowDatabaseInput {
auto: None,
reset: None,
url: None,
docker: Some(ShadowDockerInput {
version: None,
image: Some("postgis/postgis:16-3.5".to_string()),
platform: Some("linux/amd64".to_string()),
environment: None,
container_name: None,
auto_cleanup: None,
volumes: None,
network: None,
}),
}),
}),
directories: None,
objects: None,
migration: None,
schema: None,
docker: None,
};
match ShadowUrlArgs::default().resolve(&config_input).unwrap() {
ShadowDatabase::Docker(docker_config) => {
assert_eq!(docker_config.resolved_image(), "postgis/postgis:16-3.5");
assert_eq!(docker_config.platform.as_deref(), Some("linux/amd64"));
}
_ => panic!("Expected Docker shadow database"),
}
}
#[test]
fn test_shadow_merge_docker_over_docker_preserves_unanswered_fields() {
let base = ShadowDatabaseInput {
auto: None,
reset: None,
url: None,
docker: Some(ShadowDockerInput {
image: Some("postgis/postgis:16-3.4".to_string()),
container_name: Some("pgmt_shadow_app".to_string()),
auto_cleanup: Some(false),
environment: Some([("POSTGRES_PASSWORD".to_string(), "secret".to_string())].into()),
..Default::default()
}),
};
let overlay = ShadowDatabaseInput {
auto: None,
reset: None,
url: None,
docker: Some(ShadowDockerInput {
image: Some("postgis/postgis:17-3.5".to_string()),
..Default::default()
}),
};
let merged = base.merge_with(overlay);
let docker = merged.docker.expect("docker mode preserved");
assert_eq!(docker.image.as_deref(), Some("postgis/postgis:17-3.5"));
assert_eq!(docker.container_name.as_deref(), Some("pgmt_shadow_app"));
assert_eq!(docker.auto_cleanup, Some(false));
assert!(docker.environment.is_some());
assert_eq!(merged.url, None);
}
#[test]
fn test_shadow_merge_mode_switch_to_auto_clears_docker() {
let base = ShadowDatabaseInput {
auto: None,
reset: None,
url: None,
docker: Some(ShadowDockerInput {
image: Some("postgis/postgis:16-3.5".to_string()),
container_name: Some("pgmt_shadow_app".to_string()),
..Default::default()
}),
};
let overlay = ShadowDatabaseInput {
auto: Some(true),
reset: None,
url: None,
docker: None,
};
let merged = base.merge_with(overlay);
assert_eq!(merged.auto, Some(true));
assert_eq!(merged.url, None);
assert!(
merged.docker.is_none(),
"stale docker block must be cleared"
);
}
#[test]
fn test_shadow_merge_mode_switch_to_url_clears_docker() {
let base = ShadowDatabaseInput {
auto: None,
reset: None,
url: None,
docker: Some(ShadowDockerInput {
image: Some("postgis/postgis:16-3.5".to_string()),
..Default::default()
}),
};
let overlay = ShadowDatabaseInput {
auto: Some(false),
reset: None,
url: Some("postgres://localhost/shadow".to_string()),
docker: None,
};
let merged = base.merge_with(overlay);
assert_eq!(merged.url.as_deref(), Some("postgres://localhost/shadow"));
assert!(
merged.docker.is_none(),
"stale docker block must be cleared"
);
}
#[test]
fn test_shadow_merge_mode_switch_url_to_docker_clears_url() {
let base = ShadowDatabaseInput {
auto: Some(false),
reset: None,
url: Some("postgres://localhost/shadow".to_string()),
docker: None,
};
let overlay = ShadowDatabaseInput {
auto: None,
reset: None,
url: None,
docker: Some(ShadowDockerInput {
image: Some("postgres:18-alpine".to_string()),
..Default::default()
}),
};
let merged = base.merge_with(overlay);
assert!(merged.url.is_none(), "stale url must be cleared");
assert_eq!(
merged.docker.and_then(|d| d.image).as_deref(),
Some("postgres:18-alpine")
);
}
#[test]
fn test_object_exclude_accepts_both_key_spellings() {
let new_style: ConfigInput = serde_yaml::from_str(
"objects:\n exclude:\n schemas: [\"pg_*\"]\n tables: [\"cache_*\"]\n",
)
.unwrap();
let legacy: ConfigInput = serde_yaml::from_str(
"objects:\n exclude:\n exclude_schemas: [\"pg_*\"]\n exclude_tables: [\"cache_*\"]\n",
)
.unwrap();
assert_eq!(new_style.objects, legacy.objects);
let exclude = new_style.objects.unwrap().exclude.unwrap();
assert_eq!(exclude.schemas, Some(vec!["pg_*".to_string()]));
assert_eq!(exclude.tables, Some(vec!["cache_*".to_string()]));
}
#[test]
fn test_shadow_url_reset_mode_resolution() {
let config_input: ConfigInput = serde_yaml::from_str(
"databases:\n dev_url: postgres://localhost/dev\n shadow:\n url: postgres://ci/shadow\n reset: branch\n",
)
.unwrap();
match ShadowUrlArgs::default().resolve(&config_input).unwrap() {
ShadowDatabase::Url { url, reset } => {
assert_eq!(url, "postgres://ci/shadow");
assert_eq!(reset, ShadowResetMode::Branch);
}
_ => panic!("Expected Url shadow"),
}
let config_input: ConfigInput = serde_yaml::from_str(
"databases:\n dev_url: postgres://localhost/dev\n shadow:\n url: postgres://ci/shadow\n",
)
.unwrap();
match ShadowUrlArgs::default().resolve(&config_input).unwrap() {
ShadowDatabase::Url { reset, .. } => assert_eq!(reset, ShadowResetMode::Clean),
_ => panic!("Expected Url shadow"),
}
}