1pub mod ais;
7pub mod bind;
8pub mod config_store;
9pub mod control;
10pub mod registry;
11pub mod resolver;
12pub mod services;
13pub mod signaling;
14pub mod signer;
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16pub mod turn;
17
18pub use crate::config::ais::AisConfig;
19pub use crate::config::bind::BindConfig;
20pub use crate::config::control::{AdminUiConfig, ControlConfig, ControlHead};
21pub use crate::config::services::ServicesConfig;
22pub use crate::config::signaling::SignalingConfig;
23pub use crate::config::turn::TurnConfig;
24use ::signer::storage::StorageBackend;
25use std::path::{Path, PathBuf};
26use url::Url;
27
28#[derive(Debug, Serialize, Deserialize, Clone)]
33pub struct ActrixConfig {
34 #[serde(default = "default_enable")]
51 pub enable: u8,
52
53 pub name: String,
58
59 pub env: String,
66
67 pub user: Option<String>,
72
73 pub group: Option<String>,
77
78 pub pid: Option<String>,
83
84 pub bind: BindConfig,
88
89 pub turn: TurnConfig,
93
94 pub location_tag: String,
99
100 #[serde(default)]
104 pub control: ControlConfig,
105
106 #[serde(default)]
111 pub services: ServicesConfig,
112
113 #[serde(
119 serialize_with = "serialize_pathbuf",
120 deserialize_with = "deserialize_pathbuf"
121 )]
122 pub sqlite_path: PathBuf,
123
124 pub actrix_shared_key: String,
135
136 #[serde(default)]
140 pub recording: RecordingConfig,
141
142 #[serde(default)]
149 pub monitoring: MonitoringConfig,
150}
151
152#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
154pub struct RecordingConfig {
155 #[serde(default)]
164 pub sink: Option<String>,
165
166 #[serde(default = "default_recording_service_name")]
168 pub service_name: String,
169
170 #[serde(default = "default_observability_channel")]
172 pub observability: RecordingChannelConfig,
173 #[serde(default = "default_audit_channel")]
175 pub audit: RecordingChannelConfig,
176 #[serde(default = "default_security_channel")]
178 pub security: RecordingChannelConfig,
179 #[serde(default = "default_operations_channel")]
181 pub operations: RecordingChannelConfig,
182}
183
184#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
186pub struct RecordingChannelConfig {
187 #[serde(default)]
195 pub filter: String,
196
197 #[serde(default)]
204 pub sink: Option<String>,
205}
206
207fn default_audit_channel() -> RecordingChannelConfig {
208 RecordingChannelConfig {
209 filter: "mutations".to_string(),
210 sink: None,
211 }
212}
213
214fn default_security_channel() -> RecordingChannelConfig {
215 RecordingChannelConfig {
216 filter: "all".to_string(),
217 sink: None,
218 }
219}
220
221fn default_operations_channel() -> RecordingChannelConfig {
222 RecordingChannelConfig {
223 filter: "lifecycle".to_string(),
224 sink: None,
225 }
226}
227
228fn default_observability_channel() -> RecordingChannelConfig {
229 RecordingChannelConfig {
230 filter: "digest".to_string(),
231 sink: None,
232 }
233}
234
235impl Default for RecordingConfig {
236 fn default() -> Self {
237 Self {
238 sink: None,
239 service_name: default_recording_service_name(),
240 observability: default_observability_channel(),
241 audit: default_audit_channel(),
242 security: default_security_channel(),
243 operations: default_operations_channel(),
244 }
245 }
246}
247
248fn normalized_optional_uri(uri: &Option<String>) -> Option<String> {
249 uri.as_deref()
250 .map(str::trim)
251 .filter(|value| !value.is_empty())
252 .map(ToString::to_string)
253}
254
255fn any_recording_file_sink(recording: &RecordingConfig) -> bool {
256 [
257 &recording.sink,
258 &recording.observability.sink,
259 &recording.audit.sink,
260 &recording.security.sink,
261 &recording.operations.sink,
262 ]
263 .into_iter()
264 .filter_map(normalized_optional_uri)
265 .any(|sink| sink.starts_with("file://"))
266}
267
268fn validate_recording_sink_field(field: &str, value: &Option<String>, errors: &mut Vec<String>) {
269 let Some(uri) = normalized_optional_uri(value) else {
270 return;
271 };
272
273 let parsed = match Url::parse(&uri) {
274 Ok(parsed) => parsed,
275 Err(error) => {
276 errors.push(format!(
277 "Invalid URI in {field}: {uri} (parse error: {error})"
278 ));
279 return;
280 }
281 };
282
283 match parsed.scheme() {
284 "file" => {
285 if parsed.to_file_path().is_err() {
286 errors.push(format!(
287 "Invalid file URI in {field}: {uri} (cannot convert to local file path)"
288 ));
289 }
290 }
291 "otlp+http" | "otlp+grpc" => {
292 if parsed.host_str().is_none() {
293 errors.push(format!(
294 "Invalid OTLP URI in {field}: {uri} (host is required)"
295 ));
296 }
297 }
298 _ => {
299 errors.push(format!(
300 "Invalid URI scheme in {field}: expected file://, otlp+http:// or otlp+grpc://, got {}",
301 parsed.scheme()
302 ));
303 }
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, Default)]
313pub struct MonitoringConfig {
314 #[serde(default)]
317 pub allowed_ips: Vec<String>,
318
319 #[serde(default)]
322 pub htpasswd_file: String,
323
324 #[serde(default)]
327 pub services: std::collections::HashMap<String, MonitoringServiceOverride>,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, Default)]
332pub struct MonitoringServiceOverride {
333 #[serde(default)]
334 pub allowed_ips: Vec<String>,
335 #[serde(default)]
336 pub htpasswd_file: String,
337}
338
339impl MonitoringConfig {
340 pub fn is_open(&self) -> bool {
342 self.allowed_ips.is_empty() && self.htpasswd_file.is_empty()
343 }
344
345 pub fn effective_for(&self, service: &str) -> (&[String], &str) {
348 if let Some(ovr) = self.services.get(service) {
349 (ovr.allowed_ips.as_slice(), ovr.htpasswd_file.as_str())
350 } else {
351 (self.allowed_ips.as_slice(), self.htpasswd_file.as_str())
352 }
353 }
354
355 pub fn is_open_for(&self, service: &str) -> bool {
357 let (ips, htpasswd) = self.effective_for(service);
358 ips.is_empty() && htpasswd.is_empty()
359 }
360}
361
362fn default_enable() -> u8 {
363 31 }
365
366fn default_recording_service_name() -> String {
367 "actrix".to_string()
368}
369
370fn serialize_pathbuf<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
371where
372 S: Serializer,
373{
374 path.display().to_string().serialize(serializer)
375}
376
377fn deserialize_pathbuf<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
378where
379 D: Deserializer<'de>,
380{
381 let s = String::deserialize(deserializer)?;
382 Ok(PathBuf::from(s))
383}
384
385impl Default for ActrixConfig {
386 fn default() -> Self {
387 Self {
388 enable: default_enable(), name: "actrix-default".to_string(),
390 env: "dev".to_string(),
391 user: None,
392 group: None,
393 pid: Some("logs/actrix.pid".to_string()),
394 bind: BindConfig::default(),
395 turn: TurnConfig::default(),
396 location_tag: "default-location".to_string(),
397 control: ControlConfig::default(),
398 services: ServicesConfig::default(),
399 sqlite_path: PathBuf::from("database"),
400 actrix_shared_key: "XDDYE8d+yMfdXcdWMrXprcUk2uzjnmoX6nCfFw1gGIg=".to_string(),
401 recording: RecordingConfig::default(),
402 monitoring: MonitoringConfig::default(),
403 }
404 }
405}
406
407pub const ENABLE_SIGNALING: u8 = 0b00001;
409pub const ENABLE_STUN: u8 = 0b00010;
410pub const ENABLE_TURN: u8 = 0b00100;
411pub const ENABLE_AIS: u8 = 0b01000;
412pub const ENABLE_SIGNER: u8 = 0b10000;
413
414impl ActrixConfig {
415 pub fn is_signaling_enabled(&self) -> bool {
419 self.enable & ENABLE_SIGNALING != 0
420 }
421
422 pub fn is_stun_enabled(&self) -> bool {
424 self.enable & ENABLE_STUN != 0
425 }
426
427 pub fn is_turn_enabled(&self) -> bool {
429 self.enable & ENABLE_TURN != 0
430 }
431
432 pub fn is_ais_enabled(&self) -> bool {
436 self.enable & ENABLE_AIS != 0
437 }
438
439 pub fn is_signer_enabled(&self) -> bool {
443 self.enable & ENABLE_SIGNER != 0
444 }
445
446 pub fn is_ice_enabled(&self) -> bool {
448 self.is_stun_enabled() || self.is_turn_enabled()
449 }
450
451 pub fn control_head(&self) -> ControlHead {
453 if self.control.grpc_api_enabled() && !self.control.admin_ui_enabled() {
454 ControlHead::GrpcApi
455 } else {
456 ControlHead::AdminUi
457 }
458 }
459
460 pub fn admin_ui_enabled(&self) -> bool {
462 self.control.admin_ui_enabled()
463 }
464
465 pub fn grpc_api_enabled(&self) -> bool {
467 self.control.grpc_api_enabled()
468 }
469
470 pub fn superv_managed(&self) -> bool {
472 self.control.superv_managed()
473 }
474
475 pub fn get_pid_path(&self) -> Option<String> {
477 self.pid.clone().or_else(|| {
478 Some("logs/actrix.pid".to_string())
480 })
481 }
482
483 pub fn get_actrix_shared_key(&self) -> &str {
490 &self.actrix_shared_key
491 }
492
493 pub fn recording_config(&self) -> &RecordingConfig {
495 &self.recording
496 }
497
498 pub fn recording_service_name(&self) -> &str {
500 &self.recording.service_name
501 }
502
503 pub fn get_filter_level(&self) -> String {
509 if let Ok(rust_log) = std::env::var("RUST_LOG") {
510 let trimmed = rust_log.trim().to_string();
511 if !trimmed.is_empty() {
512 return trimmed;
513 }
514 }
515
516 "info,actrix::observability=trace,actrix::audit=trace,actrix::security=trace,actrix::operations=trace".to_string()
517 }
518
519 pub fn from_file<P: AsRef<std::path::Path>>(
521 path: P,
522 ) -> Result<Self, Box<dyn std::error::Error>> {
523 let path_ref = path.as_ref();
524
525 if !path_ref.exists() {
527 return Err(format!("Configuration file does not exist: {path_ref:?}").into());
528 }
529
530 if !path_ref.is_file() {
532 return Err(format!("Path is not a valid file: {path_ref:?}").into());
533 }
534
535 let content = std::fs::read_to_string(path_ref)?;
537
538 let config: ActrixConfig = toml::from_str(&content)?;
540
541 Ok(config)
542 }
543
544 pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
546 toml::from_str(content)
547 }
548
549 pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
551 toml::to_string(self)
552 }
553
554 pub fn validate(&self) -> Result<(), Vec<String>> {
562 let mut errors = Vec::new();
563
564 if self.enable > 31 {
566 errors.push(format!(
567 "Invalid enable bitmask value: {}. Must be between 0 and 31 (5 bits)",
568 self.enable
569 ));
570 }
571
572 if self.name.trim().is_empty() {
574 errors.push("Instance name cannot be empty".to_string());
575 }
576
577 if !["dev", "prod", "test"].contains(&self.env.as_str()) {
579 errors.push(format!(
580 "Invalid environment '{}', must be one of: dev, prod, test",
581 self.env
582 ));
583 }
584
585 if self.bind.http.is_none() {
587 errors.push(
588 "Control plane requires bind.http because /admin is always served".to_string(),
589 );
590 }
591
592 {
594 let check = |name: &str, value: &str, valid: &[&str]| -> Option<String> {
595 if value.is_empty() || valid.contains(&value) {
596 None
597 } else {
598 Some(format!(
599 "Invalid recording.{name}.filter '{value}', must be one of: {}",
600 valid.join(", ")
601 ))
602 }
603 };
604 if let Some(e) = check(
605 "observability",
606 &self.recording.observability.filter,
607 &["off", "digest", "detailed", "full"],
608 ) {
609 errors.push(e);
610 }
611 if let Some(e) = check(
612 "audit",
613 &self.recording.audit.filter,
614 &["off", "mutations", "all"],
615 ) {
616 errors.push(e);
617 }
618 if let Some(e) = check(
619 "security",
620 &self.recording.security.filter,
621 &["off", "critical", "high", "medium", "all"],
622 ) {
623 errors.push(e);
624 }
625 if let Some(e) = check(
626 "operations",
627 &self.recording.operations.filter,
628 &["off", "lifecycle", "detailed"],
629 ) {
630 errors.push(e);
631 }
632 }
633
634 if self.recording.service_name.trim().is_empty() {
635 errors.push("recording.service_name cannot be empty".to_string());
636 }
637
638 validate_recording_sink_field("recording.sink", &self.recording.sink, &mut errors);
640 validate_recording_sink_field(
641 "recording.observability.sink",
642 &self.recording.observability.sink,
643 &mut errors,
644 );
645 validate_recording_sink_field(
646 "recording.audit.sink",
647 &self.recording.audit.sink,
648 &mut errors,
649 );
650 validate_recording_sink_field(
651 "recording.security.sink",
652 &self.recording.security.sink,
653 &mut errors,
654 );
655 validate_recording_sink_field(
656 "recording.operations.sink",
657 &self.recording.operations.sink,
658 &mut errors,
659 );
660
661 if self.actrix_shared_key.contains("default") || self.actrix_shared_key.contains("change") {
663 errors.push("Security warning: actrix_shared_key appears to be a default value. Please change it!".to_string());
664 }
665 if self.actrix_shared_key.len() < 16 {
666 errors.push("Security warning: actrix_shared_key is too short, recommend at least 16 characters".to_string());
667 }
668
669 if self
671 .sqlite_path
672 .to_str()
673 .map(|s| s.trim().is_empty())
674 .unwrap_or(true)
675 {
676 errors.push("SQLite database path cannot be empty".to_string());
677 }
678
679 if self.is_ice_enabled() {
681 if self.bind.ice.advertised_ip.trim().is_empty() {
682 errors.push(
683 "bind.ice.advertised_ip is required when ICE services are enabled".to_string(),
684 );
685 }
686 if self
688 .bind
689 .ice
690 .advertised_ip
691 .parse::<std::net::IpAddr>()
692 .is_err()
693 {
694 errors.push(format!(
695 "Invalid bind.ice.advertised_ip '{}', must be a valid IP address",
696 self.bind.ice.advertised_ip
697 ));
698 }
699 }
700
701 if self.is_turn_enabled() && self.turn.realm.trim().is_empty() {
703 errors.push("TURN realm is required when TURN is enabled".to_string());
704 }
705
706 if self.is_signer_enabled() {
708 if let Some(ref signer_cfg) = self.services.signer {
709 match signer_cfg.storage.backend {
711 StorageBackend::Sqlite => {}
712 StorageBackend::Postgres => {
713 if signer_cfg.storage.postgres.is_none() {
714 errors.push(
715 "Signer is configured to use PostgreSQL but postgres config is missing"
716 .to_string(),
717 );
718 }
719 }
720 }
721 } else {
722 errors.push(
724 "Signer service is enabled (ENABLE_SIGNER bit is set) but services.signer configuration is missing".to_string(),
725 );
726 }
727 }
728
729 if self.is_ais_enabled() {
731 if let Some(ref ais) = self.services.ais {
733 if ais.get_signer_client_config(self).is_none() {
734 errors.push(
735 "AIS service is enabled but no Signer available: \
736 either configure services.ais.dependencies.signer or enable local Signer service"
737 .to_string(),
738 );
739 }
740 if ais.server.renewal_token_secret.is_empty() {
742 errors.push("services.ais.server.renewal_token_secret is required".to_string());
743 } else {
744 match base64::Engine::decode(
745 &base64::engine::general_purpose::STANDARD,
746 &ais.server.renewal_token_secret,
747 ) {
748 Ok(decoded) if decoded.len() >= 32 => {}
749 Ok(decoded) => {
750 errors.push(format!(
751 "services.ais.server.renewal_token_secret must decode to \
752 at least 32 bytes, got {} byte(s)",
753 decoded.len()
754 ));
755 }
756 Err(e) => {
757 errors.push(format!(
758 "services.ais.server.renewal_token_secret is not valid \
759 base64: {e}"
760 ));
761 }
762 }
763 }
764 } else {
765 errors.push(
767 "AIS service is enabled (ENABLE_AIS bit is set) but services.ais configuration is missing".to_string(),
768 );
769 }
770 }
771
772 if self.is_signaling_enabled() {
774 if let Some(ref signaling) = self.services.signaling {
775 if signaling.dependencies.signer.is_none()
776 && !(self.is_signer_enabled() && self.services.signer.is_some())
777 {
778 errors.push(
779 "Signaling dependencies.signer is not configured; enable Signer (bitmask + services.signer) to use local defaults"
780 .to_string(),
781 );
782 }
783
784 if signaling.dependencies.ais.is_none()
785 && !(self.is_ais_enabled() && self.services.ais.is_some())
786 {
787 errors.push(
788 "Signaling dependencies.ais is not configured; enable AIS (bitmask + services.ais) to use local defaults"
789 .to_string(),
790 );
791 }
792 } else {
793 errors.push(
794 "Signaling service is enabled (ENABLE_SIGNALING bit is set) but services.signaling configuration is missing"
795 .to_string(),
796 );
797 }
798 }
799
800 if self.env == "prod" {
802 if let Some(ref http) = self.bind.http {
804 if !http.is_tls() {
805 errors.push(
806 "Production environment should enable HTTPS (configure cert and key in bind.http)".to_string(),
807 );
808 }
809 } else {
810 errors.push("Production environment requires bind.http configuration".to_string());
811 }
812
813 if !any_recording_file_sink(&self.recording) {
815 errors.push("Warning: Production environment should configure recording.sink (file://...) or channel-specific recording.<channel>.sink".to_string());
816 }
817 }
818
819 if let Err(e) = self.control.validate() {
820 errors.push(format!("Control configuration error: {e}"));
821 }
822
823 if errors.is_empty() {
824 Ok(())
825 } else {
826 Err(errors)
827 }
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834 use ::signer::SignerServiceConfig;
835
836 fn valid_ais_server_config() -> ais::AisServerConfig {
837 ais::AisServerConfig {
838 renewal_token_secret: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string(),
839 ..Default::default()
840 }
841 }
842
843 #[test]
844 fn example_config_loads_and_validates() {
845 let path =
846 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../config.example.toml");
847 let cfg = ActrixConfig::from_file(&path).expect("example config should parse");
848 cfg.validate().expect("example config should validate");
849 assert!(
850 cfg.admin_ui_enabled(),
851 "example should have admin_ui enabled"
852 );
853 assert!(
854 !cfg.grpc_api_enabled(),
855 "example should have grpc_api disabled by default"
856 );
857 }
858
859 #[test]
860 fn test_default_config() {
861 let config = ActrixConfig::default();
862 assert_eq!(config.enable, 31); assert_eq!(config.name, "actrix-default");
864 assert_eq!(config.env, "dev");
865 assert!(config.is_signaling_enabled());
866 assert!(config.is_stun_enabled());
867 assert!(config.is_turn_enabled());
868 assert!(config.is_ais_enabled());
869 assert!(config.is_signer_enabled());
870 assert_eq!(config.recording.service_name, "actrix");
871 assert!(config.recording.sink.is_none());
872 assert_eq!(config.recording.observability.filter, "digest");
873 assert_eq!(config.recording.audit.filter, "mutations");
874 assert_eq!(config.recording.security.filter, "all");
875 assert_eq!(config.recording.operations.filter, "lifecycle");
876 }
877
878 #[test]
879 fn test_recording_global_plus_spec_deserialize() {
880 let mut config = ActrixConfig::default();
881 config.recording.sink = Some("file:///tmp/actrix.log".to_string());
882 config.recording.audit.sink = Some("otlp+http://127.0.0.1:4318/v1/logs".to_string());
883 config.recording.security.sink = Some("otlp+grpc://127.0.0.1:4317".to_string());
884
885 let toml = config.to_toml().expect("config should serialize");
886 let parsed = ActrixConfig::from_toml(&toml).expect("config should deserialize");
887 assert_eq!(
888 parsed.recording.sink.as_deref(),
889 Some("file:///tmp/actrix.log")
890 );
891 assert_eq!(
892 parsed.recording.audit.sink.as_deref(),
893 Some("otlp+http://127.0.0.1:4318/v1/logs")
894 );
895 assert_eq!(
896 parsed.recording.security.sink.as_deref(),
897 Some("otlp+grpc://127.0.0.1:4317")
898 );
899 }
900
901 #[test]
902 fn test_recording_sink_validation_rejects_invalid_scheme() {
903 let mut config = ActrixConfig::default();
904 config.recording.sink = Some("http://127.0.0.1:4317".to_string());
905
906 let errors = config.validate().expect_err("validation should fail");
907 assert!(
908 errors
909 .iter()
910 .any(|error| error.contains("Invalid URI scheme in recording.sink")),
911 "expected invalid scheme error, got: {errors:?}"
912 );
913 }
914
915 #[test]
916 fn test_recording_sink_validation_accepts_supported_schemes() {
917 let mut config = ActrixConfig {
918 enable: 0,
919 ..Default::default()
920 };
921 config.control.admin_ui.password = "testpassword123".to_string();
922 config.recording.sink = Some("file:///tmp/actrix.log".to_string());
923 config.recording.audit.sink = Some("otlp+http://127.0.0.1:4318/v1/logs".to_string());
924 config.recording.security.sink = Some("otlp+grpc://127.0.0.1:4317".to_string());
925
926 assert!(
927 config.validate().is_ok(),
928 "supported sink schemes should pass validation"
929 );
930 }
931
932 #[test]
933 fn test_recording_sink_validation_rejects_otlp_uri_without_host() {
934 let mut config = ActrixConfig::default();
935 config.recording.sink = Some("otlp+grpc:///v1/traces".to_string());
936
937 let errors = config.validate().expect_err("validation should fail");
938 assert!(
939 errors
940 .iter()
941 .any(|error| error.contains("Invalid OTLP URI in recording.sink")),
942 "expected missing-host error, got: {errors:?}"
943 );
944 }
945
946 #[test]
947 fn test_recording_sink_validation_rejects_invalid_file_uri() {
948 let mut config = ActrixConfig::default();
949 config.recording.sink = Some("file://relative-path.log".to_string());
950
951 let errors = config.validate().expect_err("validation should fail");
952 assert!(
953 errors
954 .iter()
955 .any(|error| error.contains("Invalid file URI in recording.sink")),
956 "expected invalid file-uri error, got: {errors:?}"
957 );
958 }
959
960 #[test]
961 fn test_toml_serialization() {
962 let config = ActrixConfig::default();
963 let toml_str = config.to_toml().unwrap();
964 assert!(toml_str.contains("enable = 31")); assert!(toml_str.contains("name = \"actrix-default\""));
966 assert!(
967 toml_str
968 .contains("actrix_shared_key = \"XDDYE8d+yMfdXcdWMrXprcUk2uzjnmoX6nCfFw1gGIg=\"")
969 );
970
971 let parsed_config = ActrixConfig::from_toml(&toml_str).unwrap();
972 assert_eq!(parsed_config.enable, config.enable);
973 assert_eq!(parsed_config.name, config.name);
974 assert_eq!(parsed_config.actrix_shared_key, config.actrix_shared_key);
975 }
976
977 #[test]
978 fn test_actrix_shared_key() {
979 let config = ActrixConfig::default();
980 assert_eq!(
981 config.get_actrix_shared_key(),
982 "XDDYE8d+yMfdXcdWMrXprcUk2uzjnmoX6nCfFw1gGIg="
983 );
984
985 let mut custom_config = config;
987 custom_config.actrix_shared_key = "custom-shared-key".to_string();
988 assert_eq!(custom_config.get_actrix_shared_key(), "custom-shared-key");
989 }
990
991 #[test]
992 fn test_service_flags() {
993 let mut config = ActrixConfig {
994 enable: 0,
995 ..ActrixConfig::default()
996 };
997
998 config.enable = 0;
1001 config.services.signaling = Some(SignalingConfig {
1002 server: signaling::SignalingServerConfig::default(),
1003 dependencies: signaling::SignalingDependencies::default(),
1004 });
1005 assert!(!config.is_signaling_enabled());
1006
1007 config.enable = ENABLE_SIGNALING;
1009 assert!(config.is_signaling_enabled());
1010
1011 config.services.signaling = Some(SignalingConfig {
1013 server: signaling::SignalingServerConfig::default(),
1014 dependencies: signaling::SignalingDependencies::default(),
1015 });
1016 assert!(config.is_signaling_enabled());
1017
1018 config.services.signaling = None;
1020 assert!(config.is_signaling_enabled());
1021
1022 config.enable = 0;
1024 config.services.ais = Some(AisConfig {
1025 server: valid_ais_server_config(),
1026 dependencies: ais::AisDependencies::default(),
1027 });
1028 assert!(!config.is_ais_enabled());
1029
1030 config.enable = ENABLE_AIS;
1032 assert!(config.is_ais_enabled());
1033
1034 config.services.ais = Some(AisConfig {
1036 server: valid_ais_server_config(),
1037 dependencies: ais::AisDependencies::default(),
1038 });
1039 assert!(config.is_ais_enabled());
1040
1041 config.services.ais = None;
1043 assert!(config.is_ais_enabled());
1044
1045 config.enable = 0;
1047 config.services.signer = Some(SignerServiceConfig {
1048 ..Default::default()
1049 });
1050 assert!(!config.is_signer_enabled());
1051
1052 config.enable = ENABLE_SIGNER;
1054 assert!(config.is_signer_enabled());
1055
1056 config.services.signer = Some(SignerServiceConfig {
1058 ..Default::default()
1059 });
1060 assert!(config.is_signer_enabled());
1061
1062 config.services.signer = None;
1064 assert!(config.is_signer_enabled());
1065
1066 config.enable = ENABLE_STUN;
1068 assert!(config.is_stun_enabled());
1069 assert!(config.is_ice_enabled());
1070
1071 config.enable = ENABLE_TURN;
1072 assert!(config.is_turn_enabled());
1073 assert!(config.is_ice_enabled());
1074 }
1075
1076 #[test]
1077 fn test_ais_auto_ks_config() {
1078 let mut config = ActrixConfig {
1079 enable: ENABLE_SIGNER | ENABLE_AIS,
1080 ..ActrixConfig::default()
1081 };
1082
1083 config.enable = ENABLE_SIGNER | ENABLE_AIS; config.services.signer = Some(SignerServiceConfig {
1086 ..Default::default()
1087 });
1088
1089 config.services.ais = Some(AisConfig {
1090 server: valid_ais_server_config(),
1091 dependencies: ais::AisDependencies { signer: None }, });
1093
1094 let ks_config = config
1096 .services
1097 .ais
1098 .as_ref()
1099 .unwrap()
1100 .get_signer_client_config(&config);
1101 assert!(ks_config.is_some());
1102 let ks_config = ks_config.unwrap();
1103 assert_eq!(ks_config.endpoint, "http://127.0.0.1:8080");
1105
1106 config.services.ais = Some(AisConfig {
1108 server: valid_ais_server_config(),
1109 dependencies: ais::AisDependencies {
1110 signer: Some(crate::config::signer::SignerClientConfig {
1111 endpoint: "http://remote-ks:8080".to_string(),
1112 timeout_seconds: 10,
1113 enable_tls: false,
1114 tls_domain: None,
1115 ca_cert: None,
1116 client_cert: None,
1117 client_key: None,
1118 }),
1119 },
1120 });
1121
1122 let ks_config = config
1123 .services
1124 .ais
1125 .as_ref()
1126 .unwrap()
1127 .get_signer_client_config(&config);
1128 assert!(ks_config.is_some());
1129 let ks_config = ks_config.unwrap();
1130 assert_eq!(ks_config.endpoint, "http://remote-ks:8080"); config.services.signer = None;
1134 config.services.ais = Some(AisConfig {
1135 server: valid_ais_server_config(),
1136 dependencies: ais::AisDependencies { signer: None },
1137 });
1138 config.enable = ENABLE_AIS; let ks_config = config
1141 .services
1142 .ais
1143 .as_ref()
1144 .unwrap()
1145 .get_signer_client_config(&config);
1146 assert!(ks_config.is_none());
1147 }
1148
1149 #[test]
1150 fn test_signaling_auto_ks_config() {
1151 let mut config = ActrixConfig {
1152 enable: ENABLE_SIGNER | ENABLE_SIGNALING,
1153 ..ActrixConfig::default()
1154 };
1155
1156 config.enable = ENABLE_SIGNER | ENABLE_SIGNALING; config.services.signer = Some(SignerServiceConfig {
1159 ..Default::default()
1160 });
1161
1162 config.services.signaling = Some(SignalingConfig {
1163 server: signaling::SignalingServerConfig::default(),
1164 dependencies: signaling::SignalingDependencies {
1165 signer: None,
1166 ais: None,
1167 },
1168 });
1169
1170 let ks_config = config
1172 .services
1173 .signaling
1174 .as_ref()
1175 .unwrap()
1176 .get_signer_client_config(&config);
1177 assert!(ks_config.is_some());
1178 let ks_config = ks_config.unwrap();
1179 assert_eq!(ks_config.endpoint, "http://127.0.0.1:8080");
1181 }
1182
1183 #[test]
1184 fn test_validate_bitmask_consistency() {
1185 let mut config = ActrixConfig {
1186 enable: ENABLE_AIS,
1187 ..ActrixConfig::default()
1188 };
1189
1190 config.enable = ENABLE_AIS;
1192 config.services.ais = None;
1193
1194 let result = config.validate();
1195 assert!(result.is_err());
1196 let errors = result.unwrap_err();
1197 assert!(errors.iter().any(|e| e.contains(
1198 "AIS service is enabled (ENABLE_AIS bit is set) but services.ais configuration is missing"
1199 )));
1200
1201 config.enable = ENABLE_SIGNER;
1203 config.services.ais = None;
1204 config.services.signer = None;
1205
1206 let result = config.validate();
1207 assert!(result.is_err());
1208 let errors = result.unwrap_err();
1209 assert!(errors.iter().any(|e| {
1210 e.contains("Signer service is enabled (ENABLE_SIGNER bit is set) but services.signer configuration is missing")
1211 }));
1212
1213 config.enable = ENABLE_SIGNALING;
1215 config.services.signaling = None;
1216 config.services.signer = None;
1217
1218 let result = config.validate();
1219 assert!(result.is_err());
1220 let errors = result.unwrap_err();
1221 assert!(errors.iter().any(|e| {
1222 e.contains("Signaling service is enabled (ENABLE_SIGNALING bit is set) but services.signaling configuration is missing")
1223 }));
1224
1225 config.enable = ENABLE_SIGNALING;
1228 config.services.signaling = Some(SignalingConfig {
1229 server: signaling::SignalingServerConfig::default(),
1230 dependencies: signaling::SignalingDependencies {
1231 signer: None,
1232 ais: None,
1233 },
1234 });
1235 config.services.signer = None; let result = config.validate();
1238 if let Err(errors) = result {
1241 assert!(
1242 !errors
1243 .iter()
1244 .any(|e| e.contains("Signaling service is enabled (ENABLE_SIGNALING bit is set) but services.signaling configuration is missing"))
1245 );
1246 assert!(
1247 !errors
1248 .iter()
1249 .any(|e| e.contains("bit is not set in enable bitmask"))
1250 );
1251 }
1252
1253 config.enable = ENABLE_AIS | ENABLE_SIGNER;
1255 config.services.ais = Some(AisConfig {
1256 server: valid_ais_server_config(),
1257 dependencies: ais::AisDependencies {
1258 signer: Some(crate::config::signer::SignerClientConfig {
1259 endpoint: "http://127.0.0.1:8080".to_string(),
1260 timeout_seconds: 10,
1261 enable_tls: false,
1262 tls_domain: None,
1263 ca_cert: None,
1264 client_cert: None,
1265 client_key: None,
1266 }),
1267 },
1268 });
1269 config.services.signer = Some(SignerServiceConfig {
1270 storage: ::signer::storage::StorageConfig {
1271 backend: ::signer::storage::StorageBackend::Sqlite,
1272 key_ttl_seconds: 3600,
1273 sqlite: Some(::signer::storage::SqliteConfig {}),
1274 postgres: None,
1275 },
1276 kek: None,
1277 kek_env: None,
1278 kek_file: None,
1279 tolerance_seconds: 3600,
1280 });
1281
1282 let result = config.validate();
1284 if let Err(errors) = result {
1285 assert!(
1286 !errors
1287 .iter()
1288 .any(|e| e.contains("bit is not set in enable bitmask"))
1289 );
1290 }
1291 }
1292
1293 #[test]
1294 fn test_signaling_dependencies_require_local_services_when_missing_clients() {
1295 let mut config = ActrixConfig {
1296 enable: ENABLE_SIGNALING,
1297 ..ActrixConfig::default()
1298 };
1299 config.services.signaling = Some(SignalingConfig {
1300 server: signaling::SignalingServerConfig::default(),
1301 dependencies: signaling::SignalingDependencies::default(),
1302 });
1303 config.services.signer = None;
1304 config.services.ais = None;
1305
1306 let result = config.validate();
1307 assert!(result.is_err());
1308 let errors = result.unwrap_err();
1309 assert!(
1310 errors
1311 .iter()
1312 .any(|e| e.contains("Signaling dependencies.signer is not configured"))
1313 );
1314 assert!(
1315 errors
1316 .iter()
1317 .any(|e| e.contains("Signaling dependencies.ais is not configured"))
1318 );
1319 }
1320
1321 #[test]
1322 fn test_signaling_dependencies_accept_local_services_when_enabled() {
1323 let mut config = ActrixConfig {
1324 enable: ENABLE_SIGNALING | ENABLE_SIGNER | ENABLE_AIS,
1325 ..ActrixConfig::default()
1326 };
1327 config.control.admin_ui.password = "testpassword123".to_string();
1328 config.services.signaling = Some(SignalingConfig {
1329 server: signaling::SignalingServerConfig::default(),
1330 dependencies: signaling::SignalingDependencies::default(),
1331 });
1332 config.services.signer = Some(SignerServiceConfig {
1333 ..Default::default()
1334 });
1335 config.services.ais = Some(AisConfig {
1336 server: valid_ais_server_config(),
1337 dependencies: ais::AisDependencies::default(),
1338 });
1339
1340 let result = config.validate();
1341 assert!(result.is_ok());
1342 }
1343}