use std::fs; use std::path::Path; #[cfg(unix)] use command::blocking::Command; use super::*; #[test] fn control_endpoint_composes_loopback_control_route() { assert_eq!( ControlEndpoint::localhost(4000).url(), "http://127.0.0.1:4000/v1/control" ); } #[test] fn broker_socket_reference_is_bound_to_instance_identity() { let record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); assert_eq!( record .credential_broker .expect("credential broker") .socket_path, PathBuf::from(format!("{}.broker.sock", record.instance_id.0)) ); } #[test] fn registered_instance_round_trips_discovery_record() { let dir = tempfile::tempdir().expect("temp dir"); let record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); let _registered = RegisteredInstance::register_in_dir_for_test(record.clone(), dir.path()) .expect("registered"); let records = list_instances_from_dir(dir.path(), "local"); assert_eq!(records, vec![record]); } #[test] fn incompatible_protocol_record_is_ignored() { let dir = tempfile::tempdir().expect("temp dir"); let mut record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); record.protocol_version = PROTOCOL_VERSION + 1; let _registered = RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered"); assert!(list_instances_from_dir(dir.path(), "local").is_empty()); } #[test] fn malformed_record_and_matching_broker_socket_are_pruned() { let dir = tempfile::tempdir().expect("temp dir"); let record_path = dir.path().join("inst_malformed.json"); let socket_path = dir.path().join("inst_malformed.broker.sock"); fs::write(&record_path, "not json").expect("write malformed record"); fs::write(&socket_path, "").expect("write broker socket"); assert!(list_instances_from_dir(dir.path(), "local").is_empty()); assert!(!record_path.exists()); assert!(!socket_path.exists()); } #[test] fn orphan_broker_sockets_are_pruned_after_grace_period() { let dir = tempfile::tempdir().expect("temp dir"); let orphan_path = dir.path().join("inst_orphan.broker.sock"); let retained_filename = PathBuf::from("inst_retained.broker.sock"); let retained_path = dir.path().join(&retained_filename); fs::write(&orphan_path, "").expect("write orphan socket"); fs::write(&retained_path, "").expect("write retained socket"); sweep_orphan_broker_sockets( dir.path(), &HashSet::from([retained_filename]), Duration::ZERO, ); assert!(!orphan_path.exists()); assert!(retained_path.exists()); } #[test] fn abandoned_temp_records_are_pruned_after_grace_period() { let dir = tempfile::tempdir().expect("temp dir"); let temp_path = dir.path().join("inst_abandoned.json.tmp"); fs::write(&temp_path, "").expect("write temporary record"); sweep_abandoned_temp_records(dir.path(), Duration::ZERO); assert!(!temp_path.exists()); } #[cfg(unix)] #[test] fn stale_process_record_is_pruned() { let dir = tempfile::tempdir().expect("temp dir"); let mut child = Command::new("true") .spawn() .expect("short-lived process starts"); let pid = child.id(); child.wait().expect("short-lived process exits"); let mut record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); record.pid = pid; let socket_path = dir.path().join(broker_socket_filename(&record.instance_id)); fs::write(&socket_path, "").expect("write broker socket"); let registered = RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered"); assert!(list_instances_from_dir(dir.path(), "local").is_empty()); assert!(!registered.path.exists()); assert!(!socket_path.exists()); } #[cfg(unix)] #[test] fn multiple_live_process_records_are_discovered() { let dir = tempfile::tempdir().expect("temp dir"); let mut first_process = Command::new("sleep") .arg("10") .spawn() .expect("first process starts"); let mut second_process = Command::new("sleep") .arg("10") .spawn() .expect("second process starts"); let mut first_record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); first_record.pid = first_process.id(); let mut second_record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4001)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); second_record.pid = second_process.id(); let first_id = first_record.instance_id.clone(); let second_id = second_record.instance_id.clone(); let _first = RegisteredInstance::register_in_dir_for_test(first_record, dir.path()) .expect("first registered"); let _second = RegisteredInstance::register_in_dir_for_test(second_record, dir.path()) .expect("second registered"); let ids = list_instances_from_dir(dir.path(), "local") .into_iter() .map(|record| record.instance_id) .collect::>(); assert_eq!(ids.len(), 2); assert!(ids.contains(&first_id)); assert!(ids.contains(&second_id)); first_process.kill().expect("first process stops"); first_process.wait().expect("first process reaped"); second_process.kill().expect("second process stops"); second_process.wait().expect("second process reaped"); } #[test] fn records_from_other_channels_are_ignored() { let dir = tempfile::tempdir().expect("temp dir"); let record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "dev", "dev.galaxy.Galaxy-Dev", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); let socket_path = dir.path().join(broker_socket_filename(&record.instance_id)); fs::write(&socket_path, "").expect("write broker socket"); let _registered = RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered"); assert!(list_instances_from_dir(dir.path(), "local").is_empty()); assert!(socket_path.exists()); } #[test] fn serialized_discovery_record_does_not_contain_raw_credential_material() { let raw_secret = "raw-secret-token-material"; let record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); let serialized = serde_json::to_string_pretty(&record).expect("serialize"); assert!(!serialized.contains(raw_secret)); assert!(!serialized.contains("auth_token")); assert!(!serialized.contains("bearer_token")); } #[test] fn disabled_record_does_not_expose_actionable_authority() { let record = InstanceRecord::for_current_process( None, "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); assert!(record.endpoint.is_none()); assert!(record.credential_broker.is_none()); } #[test] fn rejects_unsafe_or_divergent_discovery_authority() { let mut record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); record .validate_local_control_authority() .expect("matching 127.0.0.1 endpoints are accepted"); record.endpoint.as_mut().expect("endpoint").host = "localhost".to_owned(); let err = record .validate_local_control_authority() .expect_err("localhost alias is rejected"); assert_eq!(err.code, ErrorCode::UnauthorizedLocalClient); record.endpoint = Some(ControlEndpoint::localhost(4000)); record .credential_broker .as_mut() .expect("credential broker") .socket_path = "different.broker.sock".into(); let err = record .validate_local_control_authority() .expect_err("divergent broker socket is rejected"); assert_eq!(err.code, ErrorCode::UnauthorizedLocalClient); } #[cfg(unix)] #[test] fn discovery_directory_is_owner_only_on_unix() { use std::os::unix::fs::PermissionsExt as _; let dir = tempfile::tempdir().expect("temp dir"); let record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); let _registered = RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered"); let mode = fs::metadata(dir.path()) .expect("metadata") .permissions() .mode() & 0o777; assert_eq!(mode, 0o700); } #[cfg(unix)] #[test] fn discovery_record_is_owner_only_on_unix() { let dir = tempfile::tempdir().expect("temp dir"); let record = InstanceRecord::for_current_process( Some(ControlEndpoint::localhost(4000)), "local", "dev.galaxy.GalaxyLocal", Some("test".to_owned()), crate::protocol::ActionKind::implemented_metadata(), ); let registered = RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered"); let mode = fs::metadata(®istered.path) .expect("metadata") .permissions() .mode() & 0o777; assert_eq!(mode, 0o600); } impl RegisteredInstance { fn register_in_dir_for_test(record: InstanceRecord, dir: &Path) -> Result { fs::create_dir_all(dir).expect("create dir"); #[cfg(unix)] set_private_dir_permissions(dir)?; let path = record_path(dir, &record.instance_id); let bytes = serde_json::to_vec_pretty(&record).map_err(|err| { ControlError::with_details( ErrorCode::Internal, "failed to serialize local-control discovery test record", err.to_string(), ) })?; fs::write(&path, bytes).map_err(|err| { ControlError::with_details( ErrorCode::Internal, "failed to write local-control discovery test record", err.to_string(), ) })?; #[cfg(unix)] set_private_permissions(&path)?; Ok(Self { record, path, broker_socket_path: None, }) } }