1use std::{
35 collections::{BTreeSet, HashMap},
36 convert::TryFrom,
37 net::IpAddr,
38 path::PathBuf,
39 sync::Arc,
40 time::Duration,
41};
42
43use arc_swap::ArcSwap;
44use config::{Config, Environment, File, FileFormat};
45use openidconnect::{ClientId, ClientSecret};
46use opentalk_types_common::{features::ModuleFeatureId, users::Language};
47use rustc_hash::FxHashSet;
48use serde::{Deserialize, Deserializer};
49use snafu::{ResultExt, Snafu};
50use url::Url;
51
52#[derive(Debug, Snafu)]
53pub enum SettingsError {
54 #[snafu(display("Failed to read data as config: {}", source), context(false))]
55 BuildConfig { source: config::ConfigError },
56
57 #[snafu(display("Failed to apply configuration from {} or environment", file_name))]
58 DeserializeConfig {
59 file_name: String,
60 #[snafu(source(from(serde_path_to_error::Error<config::ConfigError>, Box::new)))]
61 source: Box<serde_path_to_error::Error<config::ConfigError>>,
62 },
63
64 #[snafu(display("Given base URL is not a base: {}", url))]
65 NotBaseUrl { url: Url },
66
67 #[snafu(display("Inconsistent configuration for OIDC and user search, check [keycloak], [endpoints], [oidc] and [user_search] sections"))]
68 InconsistentOidcAndUserSearchConfig,
69}
70
71type Result<T, E = SettingsError> = std::result::Result<T, E>;
72
73pub type SharedSettings = Arc<ArcSwap<Settings>>;
74
75#[derive(Debug, Clone, Deserialize)]
76pub struct Settings {
77 pub database: Database,
78 #[serde(default)]
79 pub keycloak: Option<Keycloak>,
80 #[serde(default)]
81 pub oidc: Option<Oidc>,
82 #[serde(default)]
83 pub user_search: Option<UserSearch>,
84 pub http: Http,
85 #[serde(default)]
86 pub turn: Option<Turn>,
87 #[serde(default)]
88 pub stun: Option<Stun>,
89 #[serde(default)]
90 pub redis: Option<RedisConfig>,
91 #[serde(default)]
92 pub rabbit_mq: RabbitMqConfig,
93 #[serde(default)]
94 pub logging: Logging,
95 #[serde(default)]
96 pub authz: Authz,
97 #[serde(default)]
98 pub avatar: Avatar,
99 #[serde(default)]
100 pub metrics: Metrics,
101
102 #[serde(default)]
103 pub etcd: Option<Etcd>,
104
105 #[serde(default)]
106 pub etherpad: Option<Etherpad>,
107
108 #[serde(default)]
109 pub spacedeck: Option<Spacedeck>,
110
111 #[serde(default)]
112 pub subroom_audio: Option<SubroomAudio>,
113
114 #[serde(default)]
115 pub reports: Option<Reports>,
116
117 #[serde(default)]
118 pub shared_folder: Option<SharedFolder>,
119
120 #[serde(default)]
121 pub call_in: Option<CallIn>,
122
123 #[serde(default)]
124 pub defaults: Defaults,
125
126 #[serde(default)]
127 pub endpoints: Endpoints,
128
129 pub minio: MinIO,
130
131 #[serde(default)]
132 pub monitoring: Option<MonitoringSettings>,
133
134 #[serde(default)]
135 pub tenants: Tenants,
136
137 #[serde(default)]
138 pub tariffs: Tariffs,
139
140 pub livekit: LiveKitSettings,
141
142 #[serde(flatten)]
143 pub extensions: HashMap<String, config::Value>,
144}
145
146#[derive(Debug, Clone)]
147struct WarningSource<T: Clone>(T);
148
149impl<T> config::Source for WarningSource<T>
150where
151 T: config::Source + Send + Sync + Clone + 'static,
152{
153 fn clone_into_box(&self) -> Box<dyn config::Source + Send + Sync> {
154 Box::new((*self).clone())
155 }
156
157 fn collect(&self) -> Result<config::Map<String, config::Value>, config::ConfigError> {
158 let values = self.0.collect()?;
159 if !values.is_empty() {
160 use owo_colors::OwoColorize as _;
161
162 anstream::eprintln!(
163 "{}: The following environment variables have been deprecated and \
164 will not work in a future release. Please change them as suggested below:",
165 "DEPRECATION WARNING".yellow().bold(),
166 );
167
168 for key in values.keys() {
169 let env_var = key.replace('.', "__").to_uppercase();
170 anstream::eprintln!(
171 "{}: rename environment variable {} to {}",
172 "DEPRECATION WARNING".yellow().bold(),
173 format!("K3K_CTRL_{}", env_var).yellow(),
174 format!("OPENTALK_CTRL_{}", env_var).green().bold(),
175 );
176 }
177 }
178
179 Ok(values)
180 }
181}
182
183#[derive(Debug, Clone, Deserialize)]
184pub struct MonitoringSettings {
185 #[serde(default = "default_monitoring_port")]
186 pub port: u16,
187 #[serde(default = "default_monitoring_addr")]
188 pub addr: IpAddr,
189}
190
191fn default_monitoring_port() -> u16 {
192 11411
193}
194
195fn default_monitoring_addr() -> IpAddr {
196 [0, 0, 0, 0].into()
197}
198
199#[derive(Debug, Clone, Deserialize)]
201pub struct OidcAndUserSearchConfiguration {
202 pub oidc: OidcConfiguration,
203 pub user_search: UserSearchConfiguration,
204}
205
206#[derive(Debug, Clone, Deserialize)]
208pub struct OidcConfiguration {
209 pub frontend: FrontendOidcConfiguration,
210 pub controller: ControllerOidcConfiguration,
211}
212
213#[derive(Debug, Clone, Deserialize)]
215pub struct FrontendOidcConfiguration {
216 pub auth_base_url: Url,
217 pub client_id: ClientId,
218}
219
220#[derive(Debug, Clone, Deserialize)]
222pub struct ControllerOidcConfiguration {
223 pub auth_base_url: Url,
224 pub client_id: ClientId,
225 pub client_secret: ClientSecret,
226}
227
228#[derive(Debug, Clone, Deserialize)]
230pub struct UserSearchConfiguration {
231 pub backend: UserSearchBackend,
232 pub api_base_url: Url,
233 pub client_id: ClientId,
234 pub client_secret: ClientSecret,
235 pub external_id_user_attribute_name: Option<String>,
236 pub users_find_behavior: UsersFindBehavior,
237}
238
239impl Settings {
240 fn build_url<I>(base_url: Url, path_segments: I) -> Result<Url>
242 where
243 I: IntoIterator,
244 I::Item: AsRef<str>,
245 {
246 let err_url = base_url.clone();
247 let mut url = base_url;
248 url.path_segments_mut()
249 .map_err(|_| SettingsError::NotBaseUrl { url: err_url })?
250 .extend(path_segments);
251 Ok(url)
252 }
253
254 pub fn build_oidc_and_user_search_configuration(
257 &self,
258 ) -> Result<OidcAndUserSearchConfiguration> {
259 let keycloak = self.keycloak.clone();
260 let disable_users_find = self.endpoints.disable_users_find;
261 let users_find_use_kc = self.endpoints.users_find_use_kc;
262 let oidc = self.oidc.clone();
263 let user_search = self.user_search.clone();
264
265 match (
266 keycloak,
267 disable_users_find,
268 users_find_use_kc,
269 oidc,
270 user_search,
271 ) {
272 (None, None, None, Some(oidc), Some(user_search)) => {
274 Self::build_new_oidc_and_user_search_configuration(oidc, user_search)
275 }
276 (Some(keycloak), _, _, None, None) => {
278 Self::build_legacy_oidc_and_user_search_configuration(
279 &keycloak,
280 disable_users_find,
281 users_find_use_kc,
282 )?
283 }
284 _ => Err(SettingsError::InconsistentOidcAndUserSearchConfig),
286 }
287 }
288
289 fn build_new_oidc_and_user_search_configuration(
291 oidc: Oidc,
292 user_search: UserSearch,
293 ) -> Result<OidcAndUserSearchConfiguration, SettingsError> {
294 let frontend_auth_base_url = oidc.frontend.authority.unwrap_or(oidc.authority.clone());
296 let frontend_client_id = oidc.frontend.client_id.clone();
297
298 let controller_auth_base_url = oidc.controller.authority.unwrap_or(oidc.authority);
300 let controller_client_id = oidc.controller.client_id.clone();
301 let controller_client_secret = oidc.controller.client_secret.clone();
302
303 let backend = user_search.backend;
305 let api_base_url = user_search.api_base_url;
306 let user_search_client_id = user_search
307 .client_id
308 .unwrap_or(controller_client_id.clone());
309 let user_search_client_secret = user_search
310 .client_secret
311 .unwrap_or(controller_client_secret.clone());
312 let external_id_user_attribute_name = user_search.external_id_user_attribute_name.clone();
313 let users_find_behavior = user_search.users_find_behavior;
314
315 let frontend = FrontendOidcConfiguration {
317 auth_base_url: frontend_auth_base_url,
318 client_id: frontend_client_id,
319 };
320 let controller = ControllerOidcConfiguration {
321 auth_base_url: controller_auth_base_url,
322 client_id: controller_client_id,
323 client_secret: controller_client_secret.clone(),
324 };
325 let oidc = OidcConfiguration {
326 frontend,
327 controller,
328 };
329 let api = UserSearchConfiguration {
330 backend,
331 api_base_url,
332 client_id: user_search_client_id,
333 client_secret: user_search_client_secret,
334 external_id_user_attribute_name,
335 users_find_behavior,
336 };
337 Ok(OidcAndUserSearchConfiguration {
338 oidc,
339 user_search: api,
340 })
341 }
342
343 fn build_legacy_oidc_and_user_search_configuration(
346 keycloak: &Keycloak,
347 disable_users_find: Option<bool>,
348 users_find_use_kc: Option<bool>,
349 ) -> Result<Result<OidcAndUserSearchConfiguration, SettingsError>, SettingsError> {
350 log::warn!(
351 "You are using deprecated OIDC and user search settings. See docs for [oidc] and [user_search] configuration sections."
352 );
353
354 let backend = UserSearchBackend::KeycloakWebapi;
356 let api_base_url = Self::build_url(
357 keycloak.base_url.clone(),
358 ["admin", "realms", &keycloak.realm],
359 )?;
360 let auth_base_url =
361 Self::build_url(keycloak.base_url.clone(), ["realms", &keycloak.realm])?;
362 let client_id = keycloak.client_id.clone();
363 let client_secret = keycloak.client_secret.clone();
364 let external_id_user_attribute_name = keycloak.external_id_user_attribute_name.clone();
365 let users_find_behavior = match (
366 disable_users_find.unwrap_or_default(),
367 users_find_use_kc.unwrap_or_default(),
368 ) {
369 (true, _) => UsersFindBehavior::Disabled,
370 (false, false) => UsersFindBehavior::FromDatabase,
371 (false, true) => UsersFindBehavior::FromUserSearchBackend,
372 };
373
374 let frontend = FrontendOidcConfiguration {
376 auth_base_url: auth_base_url.clone(),
377 client_id: client_id.clone(),
378 };
379 let controller = ControllerOidcConfiguration {
380 auth_base_url,
381 client_id: client_id.clone(),
382 client_secret: client_secret.clone().clone(),
383 };
384 let oidc = OidcConfiguration {
385 frontend,
386 controller,
387 };
388 let api = UserSearchConfiguration {
389 backend,
390 api_base_url,
391 client_id,
392 client_secret,
393 external_id_user_attribute_name,
394 users_find_behavior,
395 };
396 Ok(Ok(OidcAndUserSearchConfiguration {
397 oidc,
398 user_search: api,
399 }))
400 }
401
402 pub fn load(file_name: &str) -> Result<Self> {
405 let config = Config::builder()
406 .add_source(File::new(file_name, FileFormat::Toml))
407 .add_source(WarningSource(
408 Environment::with_prefix("K3K_CTRL")
409 .prefix_separator("_")
410 .separator("__"),
411 ))
412 .add_source(
413 Environment::with_prefix("OPENTALK_CTRL")
414 .prefix_separator("_")
415 .separator("__"),
416 )
417 .build()?;
418
419 let this: Self =
420 serde_path_to_error::deserialize(config).context(DeserializeConfigSnafu {
421 file_name: file_name.to_owned(),
422 })?;
423
424 Ok(this)
425 }
426}
427
428#[derive(Debug, Clone, Deserialize)]
429pub struct Database {
430 pub url: String,
431 #[serde(default = "default_max_connections")]
432 pub max_connections: u32,
433}
434
435fn default_max_connections() -> u32 {
436 100
437}
438
439#[derive(Debug, Clone, Deserialize)]
441pub struct Keycloak {
442 pub base_url: Url,
443 pub realm: String,
444 pub client_id: ClientId,
445 pub client_secret: ClientSecret,
446 pub external_id_user_attribute_name: Option<String>,
447}
448
449#[derive(Debug, Clone, Deserialize)]
450pub struct Oidc {
451 pub authority: Url,
452 pub frontend: OidcFrontend,
453 pub controller: OidcController,
454}
455
456#[derive(Debug, Clone, Deserialize)]
457pub struct OidcFrontend {
458 pub authority: Option<Url>,
459 pub client_id: ClientId,
460}
461
462#[derive(Debug, Clone, Deserialize)]
463pub struct OidcController {
464 pub authority: Option<Url>,
465 pub client_id: ClientId,
466 pub client_secret: ClientSecret,
467}
468
469#[derive(Debug, Clone, Deserialize)]
470pub struct UserSearch {
471 #[serde(flatten)]
472 pub backend: UserSearchBackend,
473 pub api_base_url: Url,
474 pub client_id: Option<ClientId>,
475 pub client_secret: Option<ClientSecret>,
476 pub external_id_user_attribute_name: Option<String>,
477 #[serde(flatten)]
478 pub users_find_behavior: UsersFindBehavior,
479}
480
481#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
482#[serde(rename_all = "snake_case", tag = "backend")]
483pub enum UserSearchBackend {
484 KeycloakWebapi,
485}
486
487#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
488#[serde(rename_all = "snake_case", tag = "users_find_behavior")]
489pub enum UsersFindBehavior {
490 Disabled,
491 FromDatabase,
492 FromUserSearchBackend,
493}
494
495#[derive(Debug, Clone, Deserialize)]
496pub struct Http {
497 #[serde(default = "default_http_port")]
498 pub port: u16,
499 #[serde(default)]
500 pub tls: Option<HttpTls>,
501}
502
503impl Default for Http {
504 fn default() -> Self {
505 Self {
506 port: default_http_port(),
507 tls: None,
508 }
509 }
510}
511
512const fn default_http_port() -> u16 {
513 11311
514}
515
516#[derive(Debug, Clone, Deserialize)]
517pub struct HttpTls {
518 pub certificate: PathBuf,
519 pub private_key: PathBuf,
520}
521
522#[derive(Default, Debug, Clone, Deserialize)]
523pub struct Logging {
524 pub default_directives: Option<Vec<String>>,
525
526 pub otlp_tracing_endpoint: Option<String>,
527
528 pub service_name: Option<String>,
529
530 pub service_namespace: Option<String>,
531
532 pub service_instance_id: Option<String>,
533}
534
535#[derive(Debug, Clone, Deserialize)]
536pub struct Turn {
537 #[serde(
539 deserialize_with = "duration_from_secs",
540 default = "default_turn_credential_lifetime"
541 )]
542 pub lifetime: Duration,
543 pub servers: Vec<TurnServer>,
545}
546
547impl Default for Turn {
548 fn default() -> Self {
549 Self {
550 lifetime: default_turn_credential_lifetime(),
551 servers: vec![],
552 }
553 }
554}
555
556fn default_turn_credential_lifetime() -> Duration {
557 Duration::from_secs(60)
558}
559
560#[derive(Debug, Clone, Deserialize)]
561pub struct TurnServer {
562 pub uris: Vec<String>,
564 pub pre_shared_key: String,
565}
566
567#[derive(Clone, Debug, Deserialize)]
568pub struct Stun {
569 pub uris: Vec<String>,
571}
572
573#[derive(Debug, Clone, Deserialize)]
574pub struct RedisConfig {
575 #[serde(default = "redis_default_url")]
576 pub url: url::Url,
577}
578
579impl Default for RedisConfig {
580 fn default() -> Self {
581 Self {
582 url: redis_default_url(),
583 }
584 }
585}
586
587fn redis_default_url() -> url::Url {
588 url::Url::try_from("redis://localhost:6379/").expect("Invalid default redis URL")
589}
590
591#[derive(Debug, Clone, Deserialize)]
592pub struct RabbitMqConfig {
593 #[serde(default = "rabbitmq_default_url")]
594 pub url: String,
595 #[serde(default = "rabbitmq_default_min_connections")]
596 pub min_connections: u32,
597 #[serde(default = "rabbitmq_default_max_channels")]
598 pub max_channels_per_connection: u32,
599 #[serde(default)]
601 pub mail_task_queue: Option<String>,
602
603 #[serde(default)]
605 pub recording_task_queue: Option<String>,
606}
607
608impl Default for RabbitMqConfig {
609 fn default() -> Self {
610 Self {
611 url: rabbitmq_default_url(),
612 min_connections: rabbitmq_default_min_connections(),
613 max_channels_per_connection: rabbitmq_default_max_channels(),
614 mail_task_queue: None,
615 recording_task_queue: None,
616 }
617 }
618}
619
620fn rabbitmq_default_url() -> String {
621 "amqp://guest:guest@localhost:5672".to_owned()
622}
623
624fn rabbitmq_default_min_connections() -> u32 {
625 10
626}
627
628fn rabbitmq_default_max_channels() -> u32 {
629 100
630}
631
632#[derive(Clone, Debug, Deserialize)]
633pub struct Authz {
634 #[serde(default = "authz_default_synchronize_controller")]
635 pub synchronize_controllers: bool,
636}
637
638impl Default for Authz {
639 fn default() -> Self {
640 Self {
641 synchronize_controllers: authz_default_synchronize_controller(),
642 }
643 }
644}
645
646fn authz_default_synchronize_controller() -> bool {
647 true
648}
649
650#[derive(Clone, Debug, Deserialize)]
651pub struct Etcd {
652 pub urls: Vec<url::Url>,
653}
654
655#[derive(Clone, Debug, Deserialize)]
656pub struct Etherpad {
657 pub url: url::Url,
658 pub api_key: String,
659}
660
661#[derive(Clone, Debug, Deserialize)]
662pub struct Spacedeck {
663 pub url: url::Url,
664 pub api_key: String,
665}
666
667#[derive(Clone, Debug, Deserialize)]
668pub struct SubroomAudio {
669 #[serde(default)]
670 pub enable_whisper: bool,
671}
672
673#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
674pub struct Reports {
675 pub url: url::Url,
676 #[serde(default)]
677 pub template: ReportsTemplate,
678}
679
680#[derive(Clone, Debug, Deserialize, Default, PartialEq, Eq)]
681#[serde(rename_all = "snake_case")]
682pub enum ReportsTemplate {
683 #[default]
685 BuiltIn,
686
687 Inline(String),
689}
690
691#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
692#[serde(tag = "provider", rename_all = "snake_case")]
693pub enum SharedFolder {
694 Nextcloud {
695 url: url::Url,
696 username: String,
697 password: String,
698 #[serde(default)]
699 directory: String,
700 #[serde(default)]
701 expiry: Option<u64>,
702 },
703}
704
705fn duration_from_secs<'de, D>(deserializer: D) -> Result<Duration, D::Error>
706where
707 D: Deserializer<'de>,
708{
709 let duration: u64 = Deserialize::deserialize(deserializer)?;
710
711 Ok(Duration::from_secs(duration))
712}
713
714#[derive(Clone, Debug, Deserialize)]
715pub struct Avatar {
716 #[serde(default = "default_libravatar_url")]
717 pub libravatar_url: String,
718}
719
720impl Default for Avatar {
721 fn default() -> Self {
722 Self {
723 libravatar_url: default_libravatar_url(),
724 }
725 }
726}
727
728fn default_libravatar_url() -> String {
729 "https://seccdn.libravatar.org/avatar/".into()
730}
731
732#[derive(Clone, Debug, Deserialize)]
733pub struct CallIn {
734 pub tel: String,
735 pub enable_phone_mapping: bool,
736 pub default_country_code: phonenumber::country::Id,
737}
738
739#[derive(Clone, Default, Debug, Deserialize)]
740pub struct Defaults {
741 #[serde(default = "default_user_language")]
742 pub user_language: Language,
743 #[serde(default)]
744 pub screen_share_requires_permission: bool,
745 #[serde(default)]
746 pub disabled_features: BTreeSet<ModuleFeatureId>,
747}
748
749fn default_user_language() -> Language {
750 "en-US".parse().expect("valid language")
751}
752
753#[derive(Clone, Default, Debug, Deserialize)]
754pub struct Endpoints {
755 pub disable_users_find: Option<bool>,
756 pub users_find_use_kc: Option<bool>,
757 #[serde(default)]
758 pub event_invite_external_email_address: bool,
759 #[serde(default)]
760 pub disallow_custom_display_name: bool,
761 #[serde(default)]
762 pub disable_openapi: bool,
763}
764
765#[derive(Clone, Debug, Deserialize)]
766pub struct MinIO {
767 pub uri: String,
768 pub bucket: String,
769 pub access_key: String,
770 pub secret_key: String,
771}
772
773#[derive(Debug, Default, Clone, Deserialize)]
774pub struct Metrics {
775 pub allowlist: Vec<cidr::IpInet>,
776}
777
778#[derive(Debug, Clone, Deserialize)]
779#[serde(rename_all = "snake_case", tag = "assignment")]
780pub enum TenantAssignment {
781 Static {
782 static_tenant_id: String,
783 },
784 ByExternalTenantId {
785 #[serde(default = "default_external_tenant_id_user_attribute_name")]
786 external_tenant_id_user_attribute_name: String,
787 },
788}
789
790fn default_external_tenant_id_user_attribute_name() -> String {
791 "tenant_id".to_owned()
792}
793
794impl Default for TenantAssignment {
795 fn default() -> Self {
796 Self::Static {
797 static_tenant_id: String::from("OpenTalkDefaultTenant"),
798 }
799 }
800}
801
802#[derive(Default, Debug, Clone, Deserialize)]
803pub struct Tenants {
804 #[serde(default, flatten)]
805 pub assignment: TenantAssignment,
806}
807
808#[derive(Debug, Clone, Deserialize)]
809#[serde(rename_all = "snake_case", tag = "assignment")]
810pub enum TariffAssignment {
811 Static { static_tariff_name: String },
812 ByExternalTariffId,
813}
814
815impl Default for TariffAssignment {
816 fn default() -> Self {
817 Self::Static {
818 static_tariff_name: String::from("OpenTalkDefaultTariff"),
819 }
820 }
821}
822
823#[derive(Default, Debug, Clone, Deserialize)]
824pub struct TariffStatusMapping {
825 pub downgraded_tariff_name: String,
826 pub default: FxHashSet<String>,
827 pub paid: FxHashSet<String>,
828 pub downgraded: FxHashSet<String>,
829}
830
831#[derive(Default, Debug, Clone, Deserialize)]
832pub struct Tariffs {
833 #[serde(default, flatten)]
834 pub assignment: TariffAssignment,
835
836 #[serde(default)]
837 pub status_mapping: Option<TariffStatusMapping>,
838}
839
840#[derive(Debug, Clone, Deserialize)]
841pub struct LiveKitSettings {
842 pub api_key: String,
843 pub api_secret: String,
844 pub public_url: String,
845
846 #[serde(alias = "url")]
848 pub service_url: String,
849}
850
851#[cfg(test)]
852mod tests {
853 use std::env;
854
855 use pretty_assertions::assert_eq;
856 use serde_json::json;
857
858 use super::*;
859
860 #[test]
861 fn settings_env_vars_overwrite_config() -> Result<()> {
862 let settings = Settings::load("../../extra/example.toml")?;
864
865 assert_eq!(
866 settings.database.url,
867 "postgres://postgres:password123@localhost:5432/opentalk"
868 );
869 assert_eq!(settings.http.port, 11311u16);
870
871 let env_db_url = "postgres://envtest:password@localhost:5432/opentalk".to_string();
873 let env_http_port: u16 = 8000;
874 let screen_share_requires_permission = true;
875 env::set_var("OPENTALK_CTRL_DATABASE__URL", &env_db_url);
876 env::set_var("OPENTALK_CTRL_HTTP__PORT", env_http_port.to_string());
877 env::set_var(
878 "OPENTALK_CTRL_DEFAULTS__SCREEN_SHARE_REQUIRES_PERMISSION",
879 screen_share_requires_permission.to_string(),
880 );
881
882 let settings = Settings::load("../../extra/example.toml")?;
883
884 assert_eq!(settings.database.url, env_db_url);
885 assert_eq!(settings.http.port, env_http_port);
886 assert_eq!(
887 settings.defaults.screen_share_requires_permission,
888 screen_share_requires_permission
889 );
890
891 Ok(())
892 }
893
894 #[test]
895 fn shared_folder_provider_nextcloud() {
896 let shared_folder = SharedFolder::Nextcloud {
897 url: "https://nextcloud.example.org/".parse().unwrap(),
898 username: "exampleuser".to_string(),
899 password: "v3rys3cr3t".to_string(),
900 directory: "meetings/opentalk".to_string(),
901 expiry: Some(34),
902 };
903 let json = json!({
904 "provider": "nextcloud",
905 "url": "https://nextcloud.example.org/",
906 "username": "exampleuser",
907 "password": "v3rys3cr3t",
908 "directory": "meetings/opentalk",
909 "expiry": 34,
910 });
911
912 assert_eq!(
913 serde_json::from_value::<SharedFolder>(json).unwrap(),
914 shared_folder
915 );
916 }
917
918 #[test]
919 fn meeting_report_settings() {
920 let toml_settings: Reports = toml::from_str(
921 r#"
922 url = "http://localhost"
923 "#,
924 )
925 .unwrap();
926 assert_eq!(
927 toml_settings,
928 Reports {
929 url: "http://localhost".parse().unwrap(),
930 template: ReportsTemplate::BuiltIn
931 }
932 );
933
934 let toml_settings: Reports = toml::from_str(
935 r#"
936 url = "http://localhost"
937 template.inline = "lorem ipsum"
938 "#,
939 )
940 .unwrap();
941 assert_eq!(
942 toml_settings,
943 Reports {
944 url: "http://localhost".parse().unwrap(),
945 template: ReportsTemplate::Inline("lorem ipsum".to_string())
946 }
947 );
948 }
949}