1use axum::http::{HeaderMap, header};
2use axum::{Json, Router, extract::State};
3use bhtune_core::{ControllerDirection, ControllerType, ProcessType, built_in_templates};
4use bhtune_runtime::config::{
5 DEMO_COOKIE_NAME, DEMO_CYCLES_COUNT_DEFAULT, DEMO_CYCLES_COUNT_MAX, DEMO_CYCLES_COUNT_MIN,
6 DEMO_CYCLES_SKIP_DEFAULT, DEMO_CYCLES_SKIP_MAX, DEMO_CYCLES_SKIP_MIN,
7 DEMO_NOISE_PROTECTION_SECS_DEFAULT, DEMO_NOISE_PROTECTION_SECS_MAX,
8 DEMO_NOISE_PROTECTION_SECS_MIN, DEMO_POLL_INTERVAL_MS, DEMO_RANGE_ENDPOINT_MAX,
9 DEMO_RANGE_ENDPOINT_MIN, DEMO_RANGE_HIGH, DEMO_RANGE_LOW, DEMO_RANGE_SPAN_MAX,
10 DEMO_RANGE_SPAN_MIN, DEMO_RELAY_AMP_DEFAULT, DEMO_RELAY_AMP_MAX, DEMO_RELAY_AMP_MIN,
11 DEMO_RUN_TIMEOUT_SECS, DEMO_SIM_DEAD_TIME_DEFAULT, DEMO_SIM_DEAD_TIME_MAX,
12 DEMO_SIM_DEAD_TIME_MIN, DEMO_SIM_GAIN_DEFAULT, DEMO_SIM_GAIN_MAX, DEMO_SIM_GAIN_MIN,
13 DEMO_SIM_INITIAL_VALUE_DEFAULT, DEMO_SIM_NOISE_DEFAULT, DEMO_SIM_NOISE_MAX_PV_SPAN_FRACTION,
14 DEMO_SIM_SEED_DEFAULT, DEMO_SIM_SEED_MAX, DEMO_SIM_TAU_DEFAULT, DEMO_SIM_TAU_MAX,
15 DEMO_SIM_TAU_MIN, DEMO_TAG_NAME, DEMO_TEMPLATE_NAME, DemoPolicy as RuntimeDemoPolicy,
16 ServerMode as RuntimeServerMode,
17};
18use serde::Serialize;
19use utoipa::ToSchema;
20
21use crate::error::ApiError;
22use crate::state::AppState;
23
24#[derive(Debug, Clone, Copy, Serialize, ToSchema, PartialEq, Eq, Default)]
27#[serde(rename_all = "lowercase")]
28pub enum ServerMode {
29 #[default]
30 Full,
31 Demo,
32}
33
34impl From<RuntimeServerMode> for ServerMode {
35 fn from(mode: RuntimeServerMode) -> Self {
36 match mode {
37 RuntimeServerMode::Full => Self::Full,
38 RuntimeServerMode::Demo => Self::Demo,
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, Serialize, ToSchema, PartialEq, Eq)]
45pub struct DemoPolicy {
46 #[schema(minimum = 86_400, maximum = 86_400)]
48 pub session_ttl_secs: u64,
49 #[schema(minimum = 200, maximum = 200)]
51 pub poll_interval_ms: u64,
52 #[schema(minimum = 30, maximum = 30)]
54 pub run_timeout_secs: u64,
55 #[schema(minimum = 8, maximum = 8)]
57 pub max_active_runs_global: u32,
58 #[schema(minimum = 1, maximum = 1)]
60 pub max_active_runs_per_visitor: u32,
61 #[schema(minimum = 6, maximum = 6)]
63 pub accepted_starts_per_token: u32,
64 #[schema(minimum = 6, maximum = 6)]
66 pub accepted_starts_per_client_ip: u32,
67 #[schema(minimum = 600, maximum = 600)]
69 pub accepted_start_window_secs: u64,
70 #[schema(minimum = 10, maximum = 10)]
72 pub retained_runs_per_visitor: u32,
73 pub max_runs_per_session: u32,
75 #[schema(minimum = 5_000, maximum = 5_000)]
77 pub max_tune_run_rows_global: u32,
78 #[schema(minimum = 32_768, maximum = 32_768)]
80 pub max_json_body_bytes: u64,
81 #[schema(minimum = 2, maximum = 2)]
83 pub max_sse_per_visitor: u32,
84 #[schema(minimum = 32, maximum = 32)]
86 pub max_sse_global: u32,
87 #[schema(minimum = 45, maximum = 45)]
89 pub sse_lifetime_secs: u64,
90 #[schema(minimum = 64, maximum = 64)]
92 pub ordinary_request_concurrency: u32,
93 #[schema(minimum = 10, maximum = 10)]
95 pub ordinary_request_timeout_secs: u64,
96 #[schema(minimum = 300, maximum = 300)]
98 pub cleanup_interval_secs: u64,
99}
100
101impl From<RuntimeDemoPolicy> for DemoPolicy {
102 fn from(policy: RuntimeDemoPolicy) -> Self {
103 Self {
104 session_ttl_secs: policy.session_ttl_secs,
105 poll_interval_ms: policy.poll_interval_ms,
106 run_timeout_secs: policy.run_timeout_secs,
107 max_active_runs_global: policy.max_active_runs_global,
108 max_active_runs_per_visitor: policy.max_active_runs_per_visitor,
109 accepted_starts_per_token: policy.accepted_starts_per_token,
110 accepted_starts_per_client_ip: policy.accepted_starts_per_client_ip,
111 accepted_start_window_secs: policy.accepted_start_window_secs,
112 retained_runs_per_visitor: policy.retained_runs_per_visitor,
113 max_runs_per_session: policy.max_runs_per_session,
114 max_tune_run_rows_global: policy.max_tune_run_rows_global,
115 max_json_body_bytes: policy.max_json_body_bytes,
116 max_sse_per_visitor: policy.max_sse_per_visitor,
117 max_sse_global: policy.max_sse_global,
118 sse_lifetime_secs: policy.sse_lifetime_secs,
119 ordinary_request_concurrency: policy.ordinary_request_concurrency,
120 ordinary_request_timeout_secs: policy.ordinary_request_timeout_secs,
121 cleanup_interval_secs: policy.cleanup_interval_secs,
122 }
123 }
124}
125
126#[derive(Debug, Serialize, ToSchema, PartialEq, Eq)]
127pub struct CapabilityActions {
128 pub start_simulator_tune: bool,
129 pub start_opcda_tune: bool,
130 pub cancel_run: bool,
131 pub stream_run: bool,
132 pub list_history: bool,
133 pub export_run: bool,
134 pub delete_run: bool,
135 pub edit_notes: bool,
136 pub write_pid: bool,
137 pub revert_pid: bool,
138 pub manage_templates: bool,
139 pub manage_config: bool,
140 pub browse_opc: bool,
141}
142
143impl CapabilityActions {
144 fn for_mode(mode: RuntimeServerMode) -> Self {
145 let demo = mode == RuntimeServerMode::Demo;
146 Self {
147 start_simulator_tune: true,
148 start_opcda_tune: !demo,
149 cancel_run: true,
150 stream_run: true,
151 list_history: true,
152 export_run: true,
153 delete_run: true,
154 edit_notes: !demo,
155 write_pid: !demo,
156 revert_pid: !demo,
157 manage_templates: !demo,
158 manage_config: !demo,
159 browse_opc: !demo,
160 }
161 }
162}
163
164#[derive(Debug, Serialize, ToSchema, PartialEq)]
165pub struct FloatBounds {
166 pub min: f32,
167 pub max: f32,
168 pub absolute_min: Option<f32>,
170}
171
172#[derive(Debug, Serialize, ToSchema, PartialEq, Eq)]
173pub struct IntegerBounds {
174 pub min: u64,
175 pub max: u64,
176}
177
178#[derive(Debug, Serialize, ToSchema, PartialEq, Eq)]
179pub struct ProcessControllerCompatibility {
180 pub process_type: ProcessType,
181 pub controller_types: Vec<ControllerType>,
182}
183
184#[derive(Debug, Serialize, ToSchema, PartialEq)]
185pub struct DemoSimulatorDefaults {
186 pub tag_name: String,
187 pub template: String,
188 pub controller_type: ControllerType,
189 pub direction: ControllerDirection,
190 pub pv_range: FloatBounds,
191 pub mv_range: FloatBounds,
192 pub poll_interval_ms: u64,
193 pub run_timeout_secs: u64,
194 pub relay_amp: f32,
195 pub cycles_skip: u32,
196 pub cycles_count: u32,
197 pub noise_protection_secs: u32,
198 pub sim_gain: f32,
199 pub sim_tau: f32,
200 pub sim_dead_time: f32,
201 pub sim_noise: f32,
202 pub sim_seed: u64,
203 pub sim_initial_pv: f32,
204 pub sim_initial_mv: f32,
205}
206
207#[derive(Debug, Serialize, ToSchema, PartialEq)]
208pub struct DemoSimulatorLimits {
209 pub relay_amp: FloatBounds,
210 pub cycles_skip: IntegerBounds,
211 pub cycles_count: IntegerBounds,
212 pub noise_protection_secs: IntegerBounds,
213 pub sim_gain: FloatBounds,
214 pub sim_tau: FloatBounds,
215 pub sim_dead_time: FloatBounds,
216 pub sim_seed: IntegerBounds,
217 pub range_endpoint: FloatBounds,
218 pub range_span: FloatBounds,
219 pub max_noise_fraction_of_pv_span: f32,
220}
221
222#[derive(Debug, Serialize, ToSchema, PartialEq)]
223pub struct DemoSimulatorCapabilities {
224 pub template: String,
225 pub templates: Vec<String>,
226 pub tag_name: String,
227 pub process_types: Vec<ProcessType>,
228 pub controller_types: Vec<ControllerType>,
229 pub compatibility: Vec<ProcessControllerCompatibility>,
230 pub defaults: DemoSimulatorDefaults,
231 pub limits: DemoSimulatorLimits,
232}
233
234#[derive(Debug, Serialize, ToSchema, PartialEq, Eq)]
235pub struct DemoRestrictions {
236 pub simulator_only: bool,
237 pub built_in_templates_only: bool,
238 pub fixed_tag_name: bool,
239 pub custom_tag_mappings_allowed: bool,
240 pub direction_must_match_process_gain: bool,
242 pub notes_allowed: bool,
243 pub automatic_pid_write_allowed: bool,
244 pub post_run_pid_write_allowed: bool,
245}
246
247#[derive(Debug, Serialize, ToSchema, PartialEq, Eq)]
248pub struct DemoQuotas {
249 pub max_active_runs_global: u32,
250 pub max_active_runs_per_visitor: u32,
251 pub max_runs_per_session: u32,
252 pub accepted_starts_per_token: u32,
253 pub accepted_starts_per_client_ip: u32,
254 pub accepted_start_window_secs: u64,
255 pub retained_runs_per_visitor: u32,
256 pub max_tune_run_rows_global: u32,
257 pub max_json_body_bytes: u64,
258 pub max_sse_per_visitor: u32,
259 pub max_sse_global: u32,
260 pub sse_lifetime_secs: u64,
261 pub ordinary_request_concurrency: u32,
262 pub ordinary_request_timeout_secs: u64,
263}
264
265impl From<RuntimeDemoPolicy> for DemoQuotas {
266 fn from(policy: RuntimeDemoPolicy) -> Self {
267 Self {
268 max_active_runs_global: policy.max_active_runs_global,
269 max_active_runs_per_visitor: policy.max_active_runs_per_visitor,
270 max_runs_per_session: policy.max_runs_per_session,
271 accepted_starts_per_token: policy.accepted_starts_per_token,
272 accepted_starts_per_client_ip: policy.accepted_starts_per_client_ip,
273 accepted_start_window_secs: policy.accepted_start_window_secs,
274 retained_runs_per_visitor: policy.retained_runs_per_visitor,
275 max_tune_run_rows_global: policy.max_tune_run_rows_global,
276 max_json_body_bytes: policy.max_json_body_bytes,
277 max_sse_per_visitor: policy.max_sse_per_visitor,
278 max_sse_global: policy.max_sse_global,
279 sse_lifetime_secs: policy.sse_lifetime_secs,
280 ordinary_request_concurrency: policy.ordinary_request_concurrency,
281 ordinary_request_timeout_secs: policy.ordinary_request_timeout_secs,
282 }
283 }
284}
285
286#[derive(Debug, Serialize, ToSchema, PartialEq, Eq)]
287pub struct CookieCapabilities {
288 pub name: String,
289 pub path: String,
290 pub max_age_secs: u64,
291 pub http_only: bool,
292 pub secure: bool,
293 pub same_site: String,
294}
295
296#[derive(Debug, Serialize, ToSchema, PartialEq, Eq)]
297pub struct SecurityCapabilities {
298 pub allowed_origin: String,
299 pub exact_origin_required_for_mutations: bool,
300 pub https_required: bool,
301 pub loopback_http_allowed: bool,
302 pub trusted_proxy_configured: bool,
303 pub forwarded_client_ip_header: Option<String>,
304 pub cookie: Option<CookieCapabilities>,
305}
306
307#[derive(Debug, Serialize, ToSchema)]
308pub struct CapabilitiesResponse {
309 pub mode: ServerMode,
310 pub drivers: Vec<String>,
312 pub actions: CapabilityActions,
313 pub demo: bool,
314 pub demo_policy: Option<DemoPolicy>,
315 pub simulator: Option<DemoSimulatorCapabilities>,
316 pub restrictions: Option<DemoRestrictions>,
317 pub quotas: Option<DemoQuotas>,
318 pub security: SecurityCapabilities,
319}
320
321fn compatibility() -> Vec<ProcessControllerCompatibility> {
322 ProcessType::ALL
323 .into_iter()
324 .map(|process_type| ProcessControllerCompatibility {
325 process_type,
326 controller_types: ControllerType::ALL
327 .into_iter()
328 .filter(|controller_type| controller_type.is_allowed_for(process_type))
329 .collect(),
330 })
331 .collect()
332}
333
334fn demo_simulator() -> DemoSimulatorCapabilities {
335 let process_types = ProcessType::ALL.to_vec();
336 DemoSimulatorCapabilities {
337 template: DEMO_TEMPLATE_NAME.to_owned(),
338 tag_name: DEMO_TAG_NAME.to_owned(),
339 controller_types: ControllerType::ALL.to_vec(),
340 compatibility: compatibility(),
341 defaults: DemoSimulatorDefaults {
342 tag_name: DEMO_TAG_NAME.to_owned(),
343 template: DEMO_TEMPLATE_NAME.to_owned(),
344 controller_type: ControllerType::Pi,
345 direction: ControllerDirection::Reverse,
346 pv_range: FloatBounds {
347 min: DEMO_RANGE_LOW,
348 max: DEMO_RANGE_HIGH,
349 absolute_min: None,
350 },
351 mv_range: FloatBounds {
352 min: DEMO_RANGE_LOW,
353 max: DEMO_RANGE_HIGH,
354 absolute_min: None,
355 },
356 poll_interval_ms: DEMO_POLL_INTERVAL_MS,
357 run_timeout_secs: DEMO_RUN_TIMEOUT_SECS,
358 relay_amp: DEMO_RELAY_AMP_DEFAULT,
359 cycles_skip: DEMO_CYCLES_SKIP_DEFAULT,
360 cycles_count: DEMO_CYCLES_COUNT_DEFAULT,
361 noise_protection_secs: DEMO_NOISE_PROTECTION_SECS_DEFAULT,
362 sim_gain: DEMO_SIM_GAIN_DEFAULT,
363 sim_tau: DEMO_SIM_TAU_DEFAULT,
364 sim_dead_time: DEMO_SIM_DEAD_TIME_DEFAULT,
365 sim_noise: DEMO_SIM_NOISE_DEFAULT,
366 sim_seed: DEMO_SIM_SEED_DEFAULT,
367 sim_initial_pv: DEMO_SIM_INITIAL_VALUE_DEFAULT,
368 sim_initial_mv: DEMO_SIM_INITIAL_VALUE_DEFAULT,
369 },
370 limits: DemoSimulatorLimits {
371 relay_amp: FloatBounds {
372 min: DEMO_RELAY_AMP_MIN,
373 max: DEMO_RELAY_AMP_MAX,
374 absolute_min: None,
375 },
376 cycles_skip: IntegerBounds {
377 min: u64::from(DEMO_CYCLES_SKIP_MIN),
378 max: u64::from(DEMO_CYCLES_SKIP_MAX),
379 },
380 cycles_count: IntegerBounds {
381 min: u64::from(DEMO_CYCLES_COUNT_MIN),
382 max: u64::from(DEMO_CYCLES_COUNT_MAX),
383 },
384 noise_protection_secs: IntegerBounds {
385 min: u64::from(DEMO_NOISE_PROTECTION_SECS_MIN),
386 max: u64::from(DEMO_NOISE_PROTECTION_SECS_MAX),
387 },
388 sim_gain: FloatBounds {
389 min: DEMO_SIM_GAIN_MIN,
390 max: DEMO_SIM_GAIN_MAX,
391 absolute_min: None,
392 },
393 sim_tau: FloatBounds {
394 min: DEMO_SIM_TAU_MIN,
395 max: DEMO_SIM_TAU_MAX,
396 absolute_min: None,
397 },
398 sim_dead_time: FloatBounds {
399 min: DEMO_SIM_DEAD_TIME_MIN,
400 max: DEMO_SIM_DEAD_TIME_MAX,
401 absolute_min: None,
402 },
403 sim_seed: IntegerBounds {
404 min: 0,
405 max: DEMO_SIM_SEED_MAX,
406 },
407 range_endpoint: FloatBounds {
408 min: DEMO_RANGE_ENDPOINT_MIN,
409 max: DEMO_RANGE_ENDPOINT_MAX,
410 absolute_min: None,
411 },
412 range_span: FloatBounds {
413 min: DEMO_RANGE_SPAN_MIN,
414 max: DEMO_RANGE_SPAN_MAX,
415 absolute_min: None,
416 },
417 max_noise_fraction_of_pv_span: DEMO_SIM_NOISE_MAX_PV_SPAN_FRACTION,
418 },
419 templates: built_in_templates()
420 .into_iter()
421 .map(|template| template.name)
422 .collect(),
423 process_types,
424 }
425}
426
427#[utoipa::path(
428 get,
429 path = "/api/capabilities",
430 tag = "health",
431 responses((status = 200, body = CapabilitiesResponse))
432)]
433pub(crate) async fn capabilities(
434 State(state): State<AppState>,
435 request_headers: HeaderMap,
436) -> Result<(HeaderMap, Json<CapabilitiesResponse>), ApiError> {
437 let demo = state.mode == RuntimeServerMode::Demo;
438 let _request_permit = if demo {
439 Some(crate::routes::demo::ordinary_request_permit(&state)?)
440 } else {
441 None
442 };
443 let actions = CapabilityActions::for_mode(state.mode);
444 let mut response_headers = HeaderMap::new();
445 if demo
446 && let Some(cookie) =
447 crate::routes::demo::session_cookie_header(&request_headers, state.demo_policy)?
448 {
449 response_headers.insert(header::SET_COOKIE, cookie);
450 }
451 Ok((
452 response_headers,
453 Json(CapabilitiesResponse {
454 mode: state.mode.into(),
455 drivers: if demo {
456 vec!["simulator".to_owned()]
457 } else {
458 vec!["opcda".to_owned(), "simulator".to_owned()]
459 },
460 actions,
461 demo,
462 demo_policy: demo.then(|| state.demo_policy.into()),
463 simulator: demo.then(demo_simulator),
464 restrictions: demo.then_some(DemoRestrictions {
465 simulator_only: true,
466 built_in_templates_only: true,
467 fixed_tag_name: true,
468 custom_tag_mappings_allowed: false,
469 direction_must_match_process_gain: false,
470 notes_allowed: false,
471 automatic_pid_write_allowed: false,
472 post_run_pid_write_allowed: false,
473 }),
474 quotas: demo.then(|| state.demo_policy.into()),
475 security: SecurityCapabilities {
476 allowed_origin: state.allowed_origin.clone().unwrap_or_default(),
477 exact_origin_required_for_mutations: demo,
478 https_required: demo,
479 loopback_http_allowed: demo,
480 trusted_proxy_configured: state.trusted_proxy.is_some(),
481 forwarded_client_ip_header: state
482 .trusted_proxy
483 .is_some()
484 .then(|| "X-BHTune-Client-IP".to_owned()),
485 cookie: demo.then_some(CookieCapabilities {
486 name: DEMO_COOKIE_NAME.to_owned(),
487 path: "/".to_owned(),
488 max_age_secs: state.demo_policy.session_ttl_secs,
489 http_only: true,
490 secure: true,
491 same_site: "Strict".to_owned(),
492 }),
493 },
494 }),
495 ))
496}
497
498pub fn router() -> Router<AppState> {
499 Router::new().route("/api/capabilities", axum::routing::get(capabilities))
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[tokio::test]
507 async fn demo_capabilities_publish_the_authoritative_contract() {
508 let mut state = crate::test_support::in_memory_state().await;
509 state.mode = RuntimeServerMode::Demo;
510 state.allowed_origin = Some("https://demo.test".to_owned());
511 state.trusted_proxy = Some("127.0.0.1".to_owned());
512
513 let (headers, Json(response)) = capabilities(State(state.clone()), HeaderMap::new())
514 .await
515 .unwrap();
516
517 assert!(response.demo);
518 assert_eq!(response.drivers, ["simulator"]);
519 assert_eq!(
520 response.demo_policy,
521 Some(RuntimeDemoPolicy::default().into())
522 );
523 assert!(response.actions.start_simulator_tune);
524 assert!(!response.actions.start_opcda_tune);
525 assert!(!response.actions.write_pid);
526 assert!(
527 !response
528 .restrictions
529 .as_ref()
530 .unwrap()
531 .direction_must_match_process_gain
532 );
533 assert_eq!(
534 response
535 .simulator
536 .as_ref()
537 .unwrap()
538 .compatibility
539 .iter()
540 .find(|item| item.process_type == ProcessType::Flow)
541 .unwrap()
542 .controller_types,
543 [ControllerType::P, ControllerType::Pi]
544 );
545 assert_eq!(
546 response
547 .simulator
548 .as_ref()
549 .unwrap()
550 .defaults
551 .controller_type,
552 ControllerType::Pi
553 );
554 assert_eq!(
555 response.simulator.as_ref().unwrap().limits.cycles_count,
556 IntegerBounds { min: 1, max: 3 }
557 );
558 assert_eq!(
559 response.simulator.as_ref().unwrap().limits.cycles_skip,
560 IntegerBounds { min: 0, max: 2 }
561 );
562 assert_eq!(
563 response.simulator.as_ref().unwrap().limits.sim_gain,
564 FloatBounds {
565 min: 0.1,
566 max: 5.0,
567 absolute_min: None,
568 }
569 );
570 assert_eq!(
571 response.simulator.as_ref().unwrap().defaults.relay_amp,
572 10.0
573 );
574 assert_eq!(
575 response.simulator.as_ref().unwrap().defaults.cycles_count,
576 2
577 );
578 assert_eq!(response.simulator.as_ref().unwrap().defaults.sim_tau, 0.5);
579 assert_eq!(
580 response.simulator.as_ref().unwrap().defaults.sim_dead_time,
581 1.0
582 );
583 assert_eq!(
584 response.simulator.as_ref().unwrap().defaults.direction,
585 ControllerDirection::Reverse
586 );
587 assert_eq!(
588 response.simulator.as_ref().unwrap().tag_name,
589 "Simulator demo"
590 );
591 assert_eq!(
592 response.simulator.as_ref().unwrap().templates.len(),
593 built_in_templates().len()
594 );
595 assert_eq!(
596 response.security.cookie.as_ref().unwrap().name,
597 DEMO_COOKIE_NAME
598 );
599 assert_eq!(response.security.allowed_origin, "https://demo.test");
600 assert!(response.security.trusted_proxy_configured);
601 assert_eq!(
602 response.security.forwarded_client_ip_header.as_deref(),
603 Some("X-BHTune-Client-IP")
604 );
605 assert_eq!(
606 response.quotas.as_ref().unwrap().retained_runs_per_visitor,
607 RuntimeDemoPolicy::default().retained_runs_per_visitor
608 );
609 let cookie = headers[header::SET_COOKIE].to_str().unwrap();
610 assert!(cookie.starts_with("__Host-bhtune_demo_session="));
611 assert!(cookie.contains("; Path=/; Max-Age=86400; HttpOnly; SameSite=Strict; Secure"));
612 assert_eq!(
613 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM demo_sessions")
614 .fetch_one(&state.pool)
615 .await
616 .unwrap(),
617 0
618 );
619 }
620
621 #[tokio::test]
622 async fn full_capabilities_preserve_the_complete_action_surface() {
623 let state = crate::test_support::in_memory_state().await;
624
625 let (headers, Json(response)) = capabilities(State(state), HeaderMap::new()).await.unwrap();
626
627 assert!(!response.demo);
628 assert_eq!(response.drivers, ["opcda", "simulator"]);
629 assert!(response.actions.start_opcda_tune);
630 assert!(response.actions.write_pid);
631 assert!(response.actions.manage_config);
632 assert_eq!(response.demo_policy, None);
633 assert!(response.simulator.is_none());
634 assert!(response.restrictions.is_none());
635 assert!(response.quotas.is_none());
636 assert!(response.security.cookie.is_none());
637 assert!(!response.security.exact_origin_required_for_mutations);
638 assert!(!headers.contains_key(header::SET_COOKIE));
639 }
640
641 #[tokio::test]
642 async fn demo_capabilities_preserve_an_existing_valid_cookie() {
643 let mut state = crate::test_support::in_memory_state().await;
644 state.mode = RuntimeServerMode::Demo;
645 let mut headers = HeaderMap::new();
646 headers.insert(
647 header::COOKIE,
648 format!("other=value; {DEMO_COOKIE_NAME}={}", "ab".repeat(32))
649 .parse()
650 .unwrap(),
651 );
652
653 let (response_headers, _) = capabilities(State(state), headers).await.unwrap();
654
655 assert!(!response_headers.contains_key(header::SET_COOKIE));
656 }
657
658 #[tokio::test]
659 async fn demo_capabilities_replace_a_malformed_cookie() {
660 let mut state = crate::test_support::in_memory_state().await;
661 state.mode = RuntimeServerMode::Demo;
662 let mut headers = HeaderMap::new();
663 headers.insert(
664 header::COOKIE,
665 format!("{DEMO_COOKIE_NAME}=invalid").parse().unwrap(),
666 );
667
668 let (response_headers, _) = capabilities(State(state), headers).await.unwrap();
669
670 assert!(response_headers.contains_key(header::SET_COOKIE));
671 }
672}