Skip to content

Commit 831ad62

Browse files
add unit tests
1 parent 0228bab commit 831ad62

File tree

5 files changed

+768
-0
lines changed

5 files changed

+768
-0
lines changed

apps/framework-cli/src/framework/core/plan.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,4 +740,108 @@ mod tests {
740740
// Compare the tables to ensure they are identical
741741
assert_eq!(reconciled.tables.values().next().unwrap(), &table);
742742
}
743+
744+
#[tokio::test]
745+
async fn test_reconcile_preserves_cluster_name() {
746+
// Create a test table with a cluster name
747+
let mut table = create_test_table("clustered_table");
748+
table.cluster_name = Some("test_cluster".to_string());
749+
750+
// Create mock OLAP client with the table (but cluster_name will be lost in reality)
751+
let mut table_from_reality = table.clone();
752+
table_from_reality.cluster_name = None; // ClickHouse system.tables doesn't preserve this
753+
754+
let mock_client = MockOlapClient {
755+
tables: vec![table_from_reality],
756+
};
757+
758+
// Create infrastructure map with the table including cluster_name
759+
let mut infra_map = InfrastructureMap::default();
760+
infra_map
761+
.tables
762+
.insert(table.id(DEFAULT_DATABASE_NAME), table.clone());
763+
764+
// Create test project
765+
let project = create_test_project();
766+
767+
let target_table_names = HashSet::new();
768+
// Reconcile the infrastructure map
769+
let reconciled =
770+
reconcile_with_reality(&project, &infra_map, &target_table_names, mock_client)
771+
.await
772+
.unwrap();
773+
774+
// The reconciled map should preserve cluster_name from the infra map
775+
assert_eq!(reconciled.tables.len(), 1);
776+
let reconciled_table = reconciled.tables.values().next().unwrap();
777+
assert_eq!(
778+
reconciled_table.cluster_name,
779+
Some("test_cluster".to_string()),
780+
"cluster_name should be preserved from infra map"
781+
);
782+
}
783+
784+
#[tokio::test]
785+
async fn test_reconcile_with_reality_mismatched_table_preserves_cluster() {
786+
// Create a table that exists in both places but with different schemas
787+
let mut infra_table = create_test_table("mismatched_table");
788+
infra_table.cluster_name = Some("production_cluster".to_string());
789+
790+
let mut reality_table = create_test_table("mismatched_table");
791+
// Reality table has no cluster_name (as ClickHouse doesn't preserve it)
792+
reality_table.cluster_name = None;
793+
// Add a column difference to make them mismatched
794+
reality_table
795+
.columns
796+
.push(crate::framework::core::infrastructure::table::Column {
797+
name: "extra_col".to_string(),
798+
data_type: crate::framework::core::infrastructure::table::ColumnType::String,
799+
required: true,
800+
unique: false,
801+
primary_key: false,
802+
default: None,
803+
annotations: vec![],
804+
comment: None,
805+
ttl: None,
806+
});
807+
808+
// Create mock OLAP client with the reality table
809+
let mock_client = MockOlapClient {
810+
tables: vec![reality_table.clone()],
811+
};
812+
813+
// Create infrastructure map with the infra table
814+
let mut infra_map = InfrastructureMap::default();
815+
infra_map
816+
.tables
817+
.insert(infra_table.id(DEFAULT_DATABASE_NAME), infra_table.clone());
818+
819+
// Create test project
820+
let project = create_test_project();
821+
822+
let target_table_names = HashSet::new();
823+
// Reconcile the infrastructure map
824+
let reconciled =
825+
reconcile_with_reality(&project, &infra_map, &target_table_names, mock_client)
826+
.await
827+
.unwrap();
828+
829+
// The reconciled map should still have the table
830+
assert_eq!(reconciled.tables.len(), 1);
831+
let reconciled_table = reconciled.tables.values().next().unwrap();
832+
833+
// The cluster_name should be preserved from the infra map
834+
assert_eq!(
835+
reconciled_table.cluster_name,
836+
Some("production_cluster".to_string()),
837+
"cluster_name should be preserved from infra map even when schema differs"
838+
);
839+
840+
// But the columns should be updated from reality
841+
assert_eq!(
842+
reconciled_table.columns.len(),
843+
reality_table.columns.len(),
844+
"columns should be updated from reality"
845+
);
846+
}
743847
}

apps/framework-cli/src/framework/core/plan_validator.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,225 @@ pub fn validate(project: &Project, plan: &InfraPlan) -> Result<(), ValidationErr
8585

8686
Ok(())
8787
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use super::*;
92+
use crate::framework::core::infrastructure::table::{Column, ColumnType, OrderBy, Table};
93+
use crate::framework::core::infrastructure_map::{
94+
InfrastructureMap, PrimitiveSignature, PrimitiveTypes,
95+
};
96+
use crate::framework::core::partial_infrastructure_map::LifeCycle;
97+
use crate::framework::core::plan::InfraPlan;
98+
use crate::framework::versions::Version;
99+
use crate::infrastructure::olap::clickhouse::config::{ClickHouseConfig, ClusterConfig};
100+
use crate::project::{Project, ProjectFeatures};
101+
use std::collections::HashMap;
102+
use std::path::PathBuf;
103+
104+
fn create_test_project(clusters: Option<Vec<ClusterConfig>>) -> Project {
105+
Project {
106+
language: crate::framework::languages::SupportedLanguages::Typescript,
107+
redpanda_config: crate::infrastructure::stream::kafka::models::KafkaConfig::default(),
108+
clickhouse_config: ClickHouseConfig {
109+
db_name: "local".to_string(),
110+
user: "default".to_string(),
111+
password: "".to_string(),
112+
use_ssl: false,
113+
host: "localhost".to_string(),
114+
host_port: 18123,
115+
native_port: 9000,
116+
host_data_path: None,
117+
additional_databases: vec![],
118+
clusters,
119+
},
120+
http_server_config: crate::cli::local_webserver::LocalWebserverConfig::default(),
121+
redis_config: crate::infrastructure::redis::redis_client::RedisConfig::default(),
122+
git_config: crate::utilities::git::GitConfig::default(),
123+
temporal_config:
124+
crate::infrastructure::orchestration::temporal::TemporalConfig::default(),
125+
state_config: crate::project::StateConfig::default(),
126+
migration_config: crate::project::MigrationConfig::default(),
127+
language_project_config: crate::project::LanguageProjectConfig::default(),
128+
project_location: PathBuf::from("/test"),
129+
is_production: false,
130+
supported_old_versions: HashMap::new(),
131+
jwt: None,
132+
authentication: crate::project::AuthenticationConfig::default(),
133+
features: ProjectFeatures::default(),
134+
load_infra: None,
135+
typescript_config: crate::project::TypescriptConfig::default(),
136+
source_dir: crate::project::default_source_dir(),
137+
}
138+
}
139+
140+
fn create_test_table(name: &str, cluster_name: Option<String>) -> Table {
141+
Table {
142+
name: name.to_string(),
143+
columns: vec![Column {
144+
name: "id".to_string(),
145+
data_type: ColumnType::String,
146+
required: true,
147+
unique: false,
148+
primary_key: true,
149+
default: None,
150+
annotations: vec![],
151+
comment: None,
152+
ttl: None,
153+
}],
154+
order_by: OrderBy::Fields(vec!["id".to_string()]),
155+
partition_by: None,
156+
sample_by: None,
157+
engine: None,
158+
version: Some(Version::from_string("1.0.0".to_string())),
159+
source_primitive: PrimitiveSignature {
160+
name: name.to_string(),
161+
primitive_type: PrimitiveTypes::DataModel,
162+
},
163+
metadata: None,
164+
life_cycle: LifeCycle::FullyManaged,
165+
engine_params_hash: None,
166+
table_settings: None,
167+
indexes: vec![],
168+
database: None,
169+
table_ttl_setting: None,
170+
cluster_name,
171+
}
172+
}
173+
174+
fn create_test_plan(tables: Vec<Table>) -> InfraPlan {
175+
let mut table_map = HashMap::new();
176+
for table in tables {
177+
table_map.insert(format!("local_{}", table.name), table);
178+
}
179+
180+
InfraPlan {
181+
target_infra_map: InfrastructureMap {
182+
default_database: "local".to_string(),
183+
tables: table_map,
184+
topics: HashMap::new(),
185+
api_endpoints: HashMap::new(),
186+
views: HashMap::new(),
187+
topic_to_table_sync_processes: HashMap::new(),
188+
topic_to_topic_sync_processes: HashMap::new(),
189+
function_processes: HashMap::new(),
190+
block_db_processes: crate::framework::core::infrastructure::olap_process::OlapProcess {},
191+
consumption_api_web_server: crate::framework::core::infrastructure::consumption_webserver::ConsumptionApiWebServer {},
192+
orchestration_workers: HashMap::new(),
193+
sql_resources: HashMap::new(),
194+
workflows: HashMap::new(),
195+
web_apps: HashMap::new(),
196+
},
197+
changes: Default::default(),
198+
}
199+
}
200+
201+
#[test]
202+
fn test_validate_no_clusters_defined_but_table_references_one() {
203+
let project = create_test_project(None);
204+
let table = create_test_table("test_table", Some("test_cluster".to_string()));
205+
let plan = create_test_plan(vec![table]);
206+
207+
let result = validate(&project, &plan);
208+
209+
assert!(result.is_err());
210+
match result {
211+
Err(ValidationError::ClusterValidation(msg)) => {
212+
assert!(msg.contains("test_table"));
213+
assert!(msg.contains("test_cluster"));
214+
assert!(msg.contains("no clusters are defined"));
215+
}
216+
_ => panic!("Expected ClusterValidation error"),
217+
}
218+
}
219+
220+
#[test]
221+
fn test_validate_table_references_undefined_cluster() {
222+
let project = create_test_project(Some(vec![
223+
ClusterConfig {
224+
name: "cluster_a".to_string(),
225+
},
226+
ClusterConfig {
227+
name: "cluster_b".to_string(),
228+
},
229+
]));
230+
let table = create_test_table("test_table", Some("cluster_c".to_string()));
231+
let plan = create_test_plan(vec![table]);
232+
233+
let result = validate(&project, &plan);
234+
235+
assert!(result.is_err());
236+
match result {
237+
Err(ValidationError::ClusterValidation(msg)) => {
238+
assert!(msg.contains("test_table"));
239+
assert!(msg.contains("cluster_c"));
240+
assert!(msg.contains("cluster_a"));
241+
assert!(msg.contains("cluster_b"));
242+
}
243+
_ => panic!("Expected ClusterValidation error"),
244+
}
245+
}
246+
247+
#[test]
248+
fn test_validate_table_references_valid_cluster() {
249+
let project = create_test_project(Some(vec![ClusterConfig {
250+
name: "test_cluster".to_string(),
251+
}]));
252+
let table = create_test_table("test_table", Some("test_cluster".to_string()));
253+
let plan = create_test_plan(vec![table]);
254+
255+
let result = validate(&project, &plan);
256+
257+
assert!(result.is_ok());
258+
}
259+
260+
#[test]
261+
fn test_validate_table_with_no_cluster_is_allowed() {
262+
let project = create_test_project(Some(vec![ClusterConfig {
263+
name: "test_cluster".to_string(),
264+
}]));
265+
let table = create_test_table("test_table", None);
266+
let plan = create_test_plan(vec![table]);
267+
268+
let result = validate(&project, &plan);
269+
270+
assert!(result.is_ok());
271+
}
272+
273+
#[test]
274+
fn test_validate_multiple_tables_different_clusters() {
275+
let project = create_test_project(Some(vec![
276+
ClusterConfig {
277+
name: "cluster_a".to_string(),
278+
},
279+
ClusterConfig {
280+
name: "cluster_b".to_string(),
281+
},
282+
]));
283+
let table1 = create_test_table("table1", Some("cluster_a".to_string()));
284+
let table2 = create_test_table("table2", Some("cluster_b".to_string()));
285+
let plan = create_test_plan(vec![table1, table2]);
286+
287+
let result = validate(&project, &plan);
288+
289+
assert!(result.is_ok());
290+
}
291+
292+
#[test]
293+
fn test_validate_empty_clusters_list() {
294+
let project = create_test_project(Some(vec![]));
295+
let table = create_test_table("test_table", Some("test_cluster".to_string()));
296+
let plan = create_test_plan(vec![table]);
297+
298+
let result = validate(&project, &plan);
299+
300+
assert!(result.is_err());
301+
match result {
302+
Err(ValidationError::ClusterValidation(msg)) => {
303+
assert!(msg.contains("test_table"));
304+
assert!(msg.contains("test_cluster"));
305+
}
306+
_ => panic!("Expected ClusterValidation error"),
307+
}
308+
}
309+
}

0 commit comments

Comments
 (0)