1/*
2 * SPDX-FileCopyrightText: 2025 Copyright (c) Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13use std::time::Duration;
14
15use cda_interfaces::HashMap;
16use http::{HeaderMap, Method, StatusCode};
17use opensovd_cda_lib::config::configfile::Configuration;
18use serde::de::DeserializeOwned;
19use sovd_interfaces::components::ecu::modes::{self, dtcsetting};
20
21use crate::{
22 sovd::{
23 self, compute_security_key, get_ecu_component,
24 locks::{self, create_lock, lock_operation},
25 put_mode,
26 },
27 util::{
28 TestingError,
29 ecusim::{self},
30 http::{
31 QueryParams, auth_header, extract_field_from_json, response_to_json, response_to_t,
32 send_cda_request,
33 },
34 runtime::{TestRuntime, restart_cda, setup_integration_test, start_ecu_sim, stop_ecu_sim},
35 },
36};
37
38/// This ECU is missing comm parameters and thus must be configured via the configuration file.
39/// The test verifies that the ECU is reachable and reports the correct name and state.
40#[tokio::test]
41async fn test_tmcc3000_ecu_online() {
42 let (runtime, _lock) = setup_integration_test(false).await.unwrap();
43
44 let json = get_ecu_component(
45 &runtime.config,
46 sovd::ECU_TMCC3000_ENDPOINT,
47 StatusCode::OK,
48 None,
49 )
50 .await
51 .expect("TMCC3000 component should be reachable via SOVD API");
52
53 let name = json
54 .get("name")
55 .and_then(|v| v.as_str())
56 .expect("Response should contain 'name' field");
57 assert_eq!(
58 name.to_lowercase(),
59 "tmcc3000",
60 "Component name should be tmcc3000"
61 );
62}
63
64/// HOVR4000 uses a non-default protocol (`DMC_DoIP`) in its MDD. The global
65/// protocol is `UDS_Ethernet_DoIP_DOBT`, so without a per-ECU protocol override
66/// the CDA would fail to load this ECU. The test verifies that the per-ECU
67/// `protocol` config override works correctly.
68#[tokio::test]
69async fn test_hovr4000_per_ecu_protocol_override() {
70 let (runtime, _lock) = setup_integration_test(false).await.unwrap();
71
72 let json = get_ecu_component(
73 &runtime.config,
74 sovd::ECU_HOVR4000_ENDPOINT,
75 StatusCode::OK,
76 None,
77 )
78 .await
79 .expect("HOVR4000 component should be reachable when per-ECU protocol override is set");
80
81 let name = json
82 .get("name")
83 .and_then(|v| v.as_str())
84 .expect("Response should contain 'name' field");
85 assert_eq!(
86 name.to_lowercase(),
87 "hovr4000",
88 "Component name should be hovr4000"
89 );
90}
91
92/// JGWT5000 has a non-default protocol (`DMC_DoIP`) in its MDD but no per-ECU
93/// protocol override. With `ignore_protocol` enabled, `into_db_protocol` falls
94/// back to the single DB protocol and com-param lookup matches by name alone.
95#[tokio::test]
96async fn test_jgwt5000_ignore_protocol_with_db_protocol() {
97 let (runtime, _lock) = setup_integration_test(false).await.unwrap();
98
99 let json = get_ecu_component(
100 &runtime.config,
101 sovd::ECU_JGWT5000_ENDPOINT,
102 StatusCode::OK,
103 None,
104 )
105 .await
106 .expect("JGWT5000 component should be reachable with ignore_protocol and no protocol override");
107
108 let name = json
109 .get("name")
110 .and_then(|v| v.as_str())
111 .expect("Response should contain 'name' field");
112 assert_eq!(
113 name.to_lowercase(),
114 "jgwt5000",
115 "Component name should be jgwt5000"
116 );
117}
118
119#[allow(clippy::too_many_lines, reason = "Makes sense to keep test together")]
120#[tokio::test]
121async fn test_ecu_session_switching() {
122 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
123 let auth = auth_header(&runtime.config, None).await.unwrap();
124 let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
125
126 // We have no lock yet, thus the CDA should reject the request to send the key.
127 send_key(
128 "Level_5".to_owned(),
129 "0x42".to_owned(),
130 &runtime.config,
131 &auth,
132 ecu_endpoint,
133 StatusCode::FORBIDDEN,
134 )
135 .await
136 .unwrap();
137
138 // Duration::from_mins is only available in rust >= 1.91.0, we want to support 1.88.0
139 #[cfg_attr(nightly, allow(unknown_lints, clippy::duration_suboptimal_units))]
140 let expiration_timeout = Duration::from_secs(60);
141 let ecu_lock = create_lock(
142 expiration_timeout,
143 locks::ECU_ENDPOINT,
144 StatusCode::CREATED,
145 &runtime.config,
146 &auth,
147 )
148 .await;
149 let lock_id =
150 extract_field_from_json::<String>(&response_to_json(&ecu_lock).unwrap(), "id").unwrap();
151
152 // Lock the ECU
153 lock_operation(
154 locks::ECU_ENDPOINT,
155 Some(&lock_id),
156 &runtime.config,
157 &auth,
158 StatusCode::OK,
159 Method::GET,
160 )
161 .await;
162
163 force_variant_detection(&runtime.config, &auth, ecu_endpoint)
164 .await
165 .unwrap();
166
167 let ecu = ecu_status(&runtime.config, &auth, ecu_endpoint)
168 .await
169 .unwrap();
170 assert!(ecu.name.eq_ignore_ascii_case("flxc1000"));
171 assert_eq!(ecu.variant.name, "FLXC1000_App_0101".to_string());
172
173 switch_session(
174 "this status does not exist",
175 &runtime.config,
176 &auth,
177 ecu_endpoint,
178 StatusCode::NOT_FOUND,
179 )
180 .await
181 .unwrap();
182
183 // Get the active diagnostic session using the Configuration GET method.
184 let get_config_result = get_configurations(
185 &runtime.config,
186 &auth,
187 ecu_endpoint,
188 "activediagnosticsessiondataidentifier",
189 )
190 .await
191 .unwrap();
192
193 assert_eq!(
194 get_config_result.id,
195 "activediagnosticsessiondataidentifier"
196 );
197 let session_type = get_config_result
198 .data
199 .get("EcuSessionType")
200 .and_then(|v| v.as_str())
201 .expect("Missing or invalid EcuSessionType");
202 assert_eq!(session_type, "Default");
203
204 let switch_session_result = switch_session(
205 "extended",
206 &runtime.config,
207 &auth,
208 ecu_endpoint,
209 StatusCode::OK,
210 )
211 .await
212 .unwrap()
213 .unwrap();
214 assert_eq!(switch_session_result.value.to_lowercase(), "extended");
215 let session_result = session(&runtime.config, &auth, ecu_endpoint).await.unwrap();
216 assert_eq!(
217 session_result.value.map(|s| s.to_lowercase()),
218 Some("extended".to_owned())
219 );
220 assert_eq!(session_result.name, Some("Diagnostic session".to_owned()));
221
222 // After switching to extended session, fetch again using configuraion GET and verify.
223 let get_config_result = get_configurations(
224 &runtime.config,
225 &auth,
226 ecu_endpoint,
227 "activediagnosticsessiondataidentifier",
228 )
229 .await
230 .unwrap();
231
232 assert_eq!(
233 get_config_result.id,
234 "activediagnosticsessiondataidentifier"
235 );
236 let session_type = get_config_result
237 .data
238 .get("EcuSessionType")
239 .and_then(|v| v.as_str())
240 .expect("Missing or invalid EcuSessionType");
241 assert_eq!(session_type, "Extended");
242
243 // Reset the ECU using the reset service and verify the session goes back to default
244 reset_ecu(
245 "hardreset",
246 &runtime.config,
247 &auth,
248 ecu_endpoint,
249 StatusCode::NO_CONTENT,
250 )
251 .await
252 .unwrap();
253
254 let session_result_after_reset = session(&runtime.config, &auth, ecu_endpoint).await.unwrap();
255 assert_eq!(
256 session_result_after_reset.value.map(|s| s.to_lowercase()),
257 Some("default".to_owned()),
258 "Session should be back to default after hard reset"
259 );
260
261 // Switch back to extended session so the remaining test steps work
262 let switch_back_result = switch_session(
263 "extended",
264 &runtime.config,
265 &auth,
266 ecu_endpoint,
267 StatusCode::OK,
268 )
269 .await
270 .unwrap()
271 .unwrap();
272 assert_eq!(switch_back_result.value.to_lowercase(), "extended");
273
274 // switch ECU sim state to BOOT
275 ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "BOOT")
276 .await
277 .unwrap();
278 force_variant_detection(&runtime.config, &auth, ecu_endpoint)
279 .await
280 .unwrap();
281 let ecu = ecu_status(&runtime.config, &auth, ecu_endpoint)
282 .await
283 .unwrap();
284 assert_eq!(ecu.variant.name, "FLXC1000_Boot_Variant".to_string());
285
286 let seed_response = request_seed(
287 "Level_5_RequestSeed".to_owned(),
288 &runtime.config,
289 &auth,
290 ecu_endpoint,
291 )
292 .await
293 .unwrap()
294 .unwrap();
295
296 // Key is too short
297 send_key(
298 "Level_5".to_owned(),
299 "0x42".to_owned(),
300 &runtime.config,
301 &auth,
302 ecu_endpoint,
303 StatusCode::BAD_GATEWAY,
304 )
305 .await
306 .unwrap();
307
308 send_key(
309 "Level_5".to_owned(),
310 seed_response.seed.request_seed.clone(),
311 &runtime.config,
312 &auth,
313 ecu_endpoint,
314 StatusCode::BAD_GATEWAY,
315 )
316 .await
317 .unwrap();
318
319 let key = compute_security_key(&seed_response.seed.request_seed);
320
321 send_key(
322 "Level_5".to_owned(),
323 key,
324 &runtime.config,
325 &auth,
326 ecu_endpoint,
327 StatusCode::OK,
328 )
329 .await
330 .unwrap();
331 let security_result = security(&runtime.config, &auth, ecu_endpoint)
332 .await
333 .unwrap();
334 assert_eq!(security_result.value, Some("Level_5".to_owned()));
335 assert_eq!(security_result.name, Some("Security access".to_owned()));
336
337 // Delete the ECU lock
338 lock_operation(
339 locks::ECU_ENDPOINT,
340 Some(&lock_id),
341 &runtime.config,
342 &auth,
343 StatusCode::NO_CONTENT,
344 Method::DELETE,
345 )
346 .await;
347}
348
349#[tokio::test]
350async fn test_variant_detection_duplicates() {
351 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
352 let auth = auth_header(&runtime.config, None).await.unwrap();
353
354 // Switch variant, and check if the NG variant is now online.
355 ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "APPLICATION")
356 .await
357 .unwrap();
358 let ecu = ecu_status(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
359 .await
360 .unwrap();
361 assert_eq!(
362 ecu.variant.state,
363 sovd_interfaces::components::ecu::State::Online
364 );
365 assert_eq!(ecu.variant.logical_address, "0x1000");
366
367 // Switch variant, and check if the NG variant is now online.
368 ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "APPLICATION2")
369 .await
370 .unwrap();
371 force_variant_detection(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
372 .await
373 .unwrap();
374
375 validate_ecu_state(
376 runtime,
377 &auth,
378 sovd::ECU_FLXC1000_ENDPOINT,
379 sovd_interfaces::components::ecu::State::Duplicate,
380 )
381 .await;
382
383 validate_ecu_state(
384 runtime,
385 &auth,
386 sovd::ECU_FLXCNG1000_ENDPOINT,
387 sovd_interfaces::components::ecu::State::Online,
388 )
389 .await;
390
391 // No variant associated with APPLICATION3, check if both ECUs are marked as NoVariantDetected
392 ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "APPLICATION3")
393 .await
394 .unwrap();
395 force_variant_detection(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
396 .await
397 .unwrap();
398 validate_ecu_state(
399 runtime,
400 &auth,
401 sovd::ECU_FLXC1000_ENDPOINT,
402 sovd_interfaces::components::ecu::State::NoVariantDetected,
403 )
404 .await;
405 validate_ecu_state(
406 runtime,
407 &auth,
408 sovd::ECU_FLXCNG1000_ENDPOINT,
409 sovd_interfaces::components::ecu::State::NoVariantDetected,
410 )
411 .await;
412
413 // Stop sim and check if ECUs are marked as disconnected after variant detection
414 stop_ecu_sim().await.unwrap();
415 force_variant_detection(&runtime.config, &auth, sovd::ECU_FLXCNG1000_ENDPOINT)
416 .await
417 .unwrap();
418
419 validate_ecu_state(
420 runtime,
421 &auth,
422 sovd::ECU_FLXC1000_ENDPOINT,
423 sovd_interfaces::components::ecu::State::Disconnected,
424 )
425 .await;
426 validate_ecu_state(
427 runtime,
428 &auth,
429 sovd::ECU_FLXCNG1000_ENDPOINT,
430 sovd_interfaces::components::ecu::State::Disconnected,
431 )
432 .await;
433
434 // restart CDA while sim is offline and check if ECUs are marked as offline
435 restart_cda(&runtime.config).await.unwrap();
436 validate_ecu_state(
437 runtime,
438 &auth,
439 sovd::ECU_FLXC1000_ENDPOINT,
440 sovd_interfaces::components::ecu::State::Offline,
441 )
442 .await;
443 validate_ecu_state(
444 runtime,
445 &auth,
446 sovd::ECU_FLXCNG1000_ENDPOINT,
447 sovd_interfaces::components::ecu::State::Offline,
448 )
449 .await;
450
451 // restart sim and wait for ECUs to come online,
452 // status should be detected without manual variant detection
453 start_ecu_sim(&runtime.ecu_sim).await.unwrap();
454
455 // wait in loop, to check if the CDA receives the spontaneous VAM when is online
456 for attempt in 0..=5 {
457 let status = ecu_status(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
458 .await
459 .expect("failed to get ecu status");
460
461 if status.variant.state == sovd_interfaces::components::ecu::State::Online {
462 break;
463 }
464
465 assert!(
466 attempt < 5,
467 "ECU did not come online in time, status {status:?}"
468 );
469 cda_interfaces::util::tokio_ext::sleep_for(Duration::from_secs(1)).await;
470 }
471
472 validate_ecu_state(
473 runtime,
474 &auth,
475 sovd::ECU_FLXCNG1000_ENDPOINT,
476 sovd_interfaces::components::ecu::State::Duplicate,
477 )
478 .await;
479}
480
481#[tokio::test]
482#[allow(clippy::too_many_lines, reason = "Keep the test together")]
483async fn test_communication_control() {
484 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
485 let auth = auth_header(&runtime.config, None).await.unwrap();
486 let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
487
488 // Without lock, the CDA should reject the request
489 set_comm_control(
490 "EnableRxAndEnableTx",
491 None,
492 &runtime.config,
493 &auth,
494 ecu_endpoint,
495 StatusCode::FORBIDDEN,
496 )
497 .await
498 .unwrap();
499
500 // Create and acquire lock
501 // Duration::from_mins is only available in rust >= 1.91.0, we want to support 1.88.0
502 #[cfg_attr(nightly, allow(unknown_lints, clippy::duration_suboptimal_units))]
503 let expiration_timeout = Duration::from_secs(60);
504 let ecu_lock = create_lock(
505 expiration_timeout,
506 locks::ECU_ENDPOINT,
507 StatusCode::CREATED,
508 &runtime.config,
509 &auth,
510 )
511 .await;
512 let lock_id =
513 extract_field_from_json::<String>(&response_to_json(&ecu_lock).unwrap(), "id").unwrap();
514
515 // Sending an invalid value should return BAD_REQUEST with possible values
516 sovd::validate_invalid_parameter_error(
517 &runtime.config,
518 &auth,
519 ecu_endpoint,
520 "commctrl",
521 modes::commctrl::put::Request {
522 value: "invalid-value".to_owned(),
523 parameters: None,
524 },
525 &[
526 "enablerxandenabletx",
527 "enablerxanddisabletx",
528 "disablerxandenabletx",
529 "disablerxanddisabletx",
530 "enablerxanddisabletxwithenhancedaddressinformation",
531 "enablerxandtxwithenhancedaddressinformation",
532 "temporalsync",
533 ],
534 )
535 .await
536 .unwrap();
537
538 let enable_rx_and_enable_tx = "enablerxandenabletx";
539 let result = set_comm_control(
540 "EnableRxAndEnableTx",
541 None,
542 &runtime.config,
543 &auth,
544 ecu_endpoint,
545 StatusCode::OK,
546 )
547 .await
548 .unwrap()
549 .unwrap();
550 assert_eq!(result.value, "EnableRxAndEnableTx");
551
552 let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
553 .await
554 .unwrap();
555 assert_eq!(
556 current_state.value.as_ref().map(|s| s.to_lowercase()),
557 Some(enable_rx_and_enable_tx.to_owned())
558 );
559
560 let enable_rx_and_disable_tx = "enablerxanddisabletx";
561 let result = set_comm_control(
562 "EnableRxAndDisableTx",
563 None,
564 &runtime.config,
565 &auth,
566 ecu_endpoint,
567 StatusCode::OK,
568 )
569 .await
570 .unwrap()
571 .unwrap();
572 assert_eq!(result.value, "EnableRxAndDisableTx");
573
574 let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
575 .await
576 .unwrap();
577 assert_eq!(
578 current_state.value.as_ref().map(|s| s.to_lowercase()),
579 Some(enable_rx_and_disable_tx.to_owned())
580 );
581
582 let disable_rx_and_enable_tx = "disablerxandenabletx";
583 let result = set_comm_control(
584 "DisableRxAndEnableTx",
585 None,
586 &runtime.config,
587 &auth,
588 ecu_endpoint,
589 StatusCode::OK,
590 )
591 .await
592 .unwrap()
593 .unwrap();
594 assert_eq!(result.value, "DisableRxAndEnableTx");
595
596 let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
597 .await
598 .unwrap();
599 assert_eq!(
600 current_state.value.as_ref().map(|s| s.to_lowercase()),
601 Some(disable_rx_and_enable_tx.to_owned())
602 );
603
604 let disable_rx_and_disable_tx = "disablerxanddisabletx";
605 let result = set_comm_control(
606 "DisableRxAndDisableTx",
607 None,
608 &runtime.config,
609 &auth,
610 ecu_endpoint,
611 StatusCode::OK,
612 )
613 .await
614 .unwrap()
615 .unwrap();
616 assert_eq!(result.value, "DisableRxAndDisableTx");
617
618 let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
619 .await
620 .unwrap();
621 assert_eq!(
622 current_state.value.as_ref().map(|s| s.to_lowercase()),
623 Some(disable_rx_and_disable_tx.to_owned())
624 );
625
626 let enable_rx_and_disable_tx_with_enhanced =
627 "enablerxanddisabletxwithenhancedaddressinformation";
628 let result = set_comm_control(
629 "EnableRxAndDisableTxWithEnhancedAddressInformation",
630 None,
631 &runtime.config,
632 &auth,
633 ecu_endpoint,
634 StatusCode::OK,
635 )
636 .await
637 .unwrap()
638 .unwrap();
639 assert_eq!(
640 result.value,
641 "EnableRxAndDisableTxWithEnhancedAddressInformation"
642 );
643
644 let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
645 .await
646 .unwrap();
647 assert_eq!(
648 current_state.value.as_ref().map(|s| s.to_lowercase()),
649 Some(enable_rx_and_disable_tx_with_enhanced.to_owned())
650 );
651
652 let enable_rx_and_tx_with_enhanced = "enablerxandtxwithenhancedaddressinformation";
653 let result = set_comm_control(
654 "EnableRxAndTxWithEnhancedAddressInformation",
655 None,
656 &runtime.config,
657 &auth,
658 ecu_endpoint,
659 StatusCode::OK,
660 )
661 .await
662 .unwrap()
663 .unwrap();
664 assert_eq!(result.value, "EnableRxAndTxWithEnhancedAddressInformation");
665
666 let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
667 .await
668 .unwrap();
669 assert_eq!(
670 current_state.value.as_ref().map(|s| s.to_lowercase()),
671 Some(enable_rx_and_tx_with_enhanced.to_owned())
672 );
673
674 // VendorSpecific (custom TemporalSync 0x88)
675 let temporal_era_id: i32 = -1_373_112_000;
676 let mut parameters = cda_interfaces::HashMap::default();
677 parameters.insert(
678 "temporalEraId".to_string(),
679 serde_json::json!(temporal_era_id),
680 );
681
682 let temporal_sync = "temporalsync";
683 let result = set_comm_control(
684 "TemporalSync",
685 Some(parameters),
686 &runtime.config,
687 &auth,
688 ecu_endpoint,
689 StatusCode::OK,
690 )
691 .await
692 .unwrap()
693 .unwrap();
694 assert_eq!(result.value, "TemporalSync");
695
696 let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
697 .await
698 .unwrap();
699 assert_eq!(
700 current_state.value.as_ref().map(|s| s.to_lowercase()),
701 Some(temporal_sync.to_owned())
702 );
703
704 // Validate that ECU sim received and stored the temporalEraId
705 let ecu_state = ecusim::get_ecu_state(&runtime.ecu_sim, "flxc1000")
706 .await
707 .expect("Failed to get ECU sim state");
708 assert_eq!(
709 ecu_state.temporal_era_id,
710 Some(temporal_era_id),
711 "ECU sim did not store the correct temporalEraId, state={ecu_state:#?}",
712 );
713 assert_eq!(
714 ecu_state.communication_control_type,
715 Some(ecusim::CommunicationControlType::TemporalSync)
716 );
717
718 // Delete the ECU lock
719 lock_operation(
720 locks::ECU_ENDPOINT,
721 Some(&lock_id),
722 &runtime.config,
723 &auth,
724 StatusCode::NO_CONTENT,
725 Method::DELETE,
726 )
727 .await;
728
729 // After deleting lock, we should not be able to set comm control
730 set_comm_control(
731 "EnableRxAndEnableTx",
732 None,
733 &runtime.config,
734 &auth,
735 ecu_endpoint,
736 StatusCode::FORBIDDEN,
737 )
738 .await
739 .unwrap();
740}
741
742#[tokio::test]
743async fn test_boot_variant_service_inheritance() {
744 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
745 let auth = auth_header(&runtime.config, None).await.unwrap();
746 let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
747
748 // Switch ECU sim to BOOT variant
749 ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "BOOT")
750 .await
751 .unwrap();
752 force_variant_detection(&runtime.config, &auth, ecu_endpoint)
753 .await
754 .unwrap();
755
756 let ecu = ecu_status(&runtime.config, &auth, ecu_endpoint)
757 .await
758 .unwrap();
759 assert_eq!(ecu.variant.name, "FLXC1000_Boot_Variant".to_string());
760
761 let data_services = get_data_services(&runtime.config, &auth, ecu_endpoint)
762 .await
763 .unwrap();
764 let service_ids: Vec<_> = data_services
765 .items
766 .iter()
767 .map(|item| item.id.to_lowercase())
768 .collect();
769
770 // Vindataidentifier is inherited and should be present in boot.
771 assert!(
772 service_ids.contains(&"vindataidentifier".to_owned()),
773 "VIN service should be inherited from base variant, service ids {}",
774 service_ids.join(", ")
775 );
776
777 // reset ecu-sim variant
778 ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "APPLICATION")
779 .await
780 .unwrap();
781
782 // As long as test_ecu_session_switching also works we know that services
783 // specific to the boot variant are still looked up correct, otherwise we cannot find
784 // RequestSeed and SendKey services, no need to test this again here.
785}
786
787#[tokio::test]
788async fn test_ecu_session_reset_on_lock_reacquire() {
789 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
790 let auth = auth_header(&runtime.config, None).await.unwrap();
791 let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
792
793 // Create and acquire lock with 30s timeout
794 let lock_expiration_timeout = Duration::from_secs(30);
795 let ecu_lock = create_lock(
796 lock_expiration_timeout,
797 locks::ECU_ENDPOINT,
798 StatusCode::CREATED,
799 &runtime.config,
800 &auth,
801 )
802 .await;
803 let lock_id =
804 extract_field_from_json::<String>(&response_to_json(&ecu_lock).unwrap(), "id").unwrap();
805
806 // Set session with 2s expiry
807 let session_expiration = 2u64;
808 let switch_session_result: modes::security_and_session::put::Response<String> = put_mode(
809 &runtime.config,
810 &auth,
811 ecu_endpoint,
812 "session",
813 modes::security_and_session::put::Request {
814 value: "extended".to_owned(),
815 mode_expiration: Some(session_expiration),
816 key: None,
817 },
818 StatusCode::OK,
819 )
820 .await
821 .unwrap()
822 .unwrap();
823 assert_eq!(switch_session_result.value.to_lowercase(), "extended");
824
825 // Verify ECU sim is in extended session
826 let ecu_state = ecusim::get_ecu_state(&runtime.ecu_sim, "flxc1000")
827 .await
828 .expect("Failed to get ECU sim state");
829 assert_eq!(
830 ecu_state.session_state,
831 Some(ecusim::SessionState::Extended),
832 "ECU sim should be in Extended session"
833 );
834
835 // Wait for the session to expire
836 cda_interfaces::util::tokio_ext::sleep_for(Duration::from_secs(session_expiration + 1)).await;
837
838 // Check if the sim is back to default
839 let ecu_state_after_expiry = ecusim::get_ecu_state(&runtime.ecu_sim, "flxc1000")
840 .await
841 .expect("Failed to get ECU sim state after session expiry");
842
843 assert_eq!(
844 ecu_state_after_expiry.session_state,
845 Some(ecusim::SessionState::Default),
846 "ECU sim should be back to Default session after session expiry"
847 );
848
849 // Also verify through CDA API
850 let session_result_after = session(&runtime.config, &auth, ecu_endpoint).await.unwrap();
851 assert_eq!(
852 session_result_after.value.map(|s| s.to_lowercase()),
853 Some("default".to_owned())
854 );
855
856 // Delete the lock
857 lock_operation(
858 locks::ECU_ENDPOINT,
859 Some(&lock_id),
860 &runtime.config,
861 &auth,
862 StatusCode::NO_CONTENT,
863 Method::DELETE,
864 )
865 .await;
866}
867
[docs] 868/// [[ itest~sovd-api-component-sdgsd, ECU-level SDG retrieval, itest ]]
869#[tokio::test]
870async fn test_ecu_sdg_retrieval() {
871 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
872 let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
873
874 // Retrieve sdgs and verify contents
875 let params = QueryParams(HashMap::from_iter([(
876 "x-sovd2uds-includesdgs".to_string(),
877 "true".to_string(),
878 )]));
879 let data = get_ecu_component(&runtime.config, ecu_endpoint, StatusCode::OK, Some(¶ms))
880 .await
881 .unwrap();
882
883 let d = data
884 .get("data")
885 .unwrap()
886 .as_str()
887 .expect("should contain data");
888 assert_eq!(
889 d,
890 "http://localhost:20002/vehicle/v15/components/flxc1000/data"
891 );
892
893 let operations = data
894 .get("operations")
895 .unwrap()
896 .as_str()
897 .expect("should contain operations");
898 assert_eq!(
899 operations,
900 "http://localhost:20002/vehicle/v15/components/flxc1000/operations"
901 );
902
903 let configurations = data
904 .get("configurations")
905 .unwrap()
906 .as_str()
907 .expect("should contain configurations");
908 assert_eq!(
909 configurations,
910 "http://localhost:20002/vehicle/v15/components/flxc1000/configurations"
911 );
912
913 let modes = data
914 .get("modes")
915 .unwrap()
916 .as_str()
917 .expect("should contain modes");
918 assert_eq!(
919 modes,
920 "http://localhost:20002/vehicle/v15/components/flxc1000/modes"
921 );
922
923 let locks = data
924 .get("locks")
925 .unwrap()
926 .as_str()
927 .expect("should contain locks");
928 assert_eq!(
929 locks,
930 "http://localhost:20002/vehicle/v15/components/flxc1000/locks"
931 );
932
933 let faults = data
934 .get("faults")
935 .unwrap()
936 .as_str()
937 .expect("should contain faults");
938 assert_eq!(
939 faults,
940 "http://localhost:20002/vehicle/v15/components/flxc1000/faults"
941 );
942
943 let sdgs = data
944 .get("sdgs")
945 .unwrap()
946 .as_array()
947 .expect("sdgs should be an array");
948 assert_eq!(sdgs.len(), 1);
949
950 let sdg = &sdgs.first().unwrap();
951 assert_eq!(sdg.get("caption").unwrap().as_str(), Some("default_sdg"));
952 assert_eq!(sdg.get("si").unwrap().as_str(), Some("default"));
953
954 let inner = sdg
955 .get("sdgs")
956 .unwrap()
957 .as_array()
958 .expect("nested sdgs should be an array");
959 assert_eq!(inner.len(), 1);
960
961 let sd = &inner.first().unwrap();
962 assert_eq!(
963 sd.get("si").unwrap().as_str(),
964 Some("power_requirement_max")
965 );
966 assert_eq!(sd.get("value").unwrap().as_str(), Some("1.21GW"));
967}
968
[docs] 969/// [[ itest~sovd-api-component-alias-sdgsd, ECU-level SDG retrieval (alias param), itest ]]
970#[tokio::test]
971async fn test_ecu_sdg_retrieval_alias() {
972 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
973 let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
974
975 // Retrieve sdgs and verify contents
976 let params = QueryParams(HashMap::from_iter([(
977 "x-include-sdgs".to_string(),
978 "true".to_string(),
979 )]));
980 let data = get_ecu_component(&runtime.config, ecu_endpoint, StatusCode::OK, Some(¶ms))
981 .await
982 .unwrap();
983
984 let sdgs = data
985 .get("sdgs")
986 .unwrap()
987 .as_array()
988 .expect("sdgs should be an array");
989 assert_eq!(sdgs.len(), 1);
990
991 let sdg = &sdgs.first().unwrap();
992 assert_eq!(sdg.get("caption").unwrap().as_str(), Some("default_sdg"));
993 assert_eq!(sdg.get("si").unwrap().as_str(), Some("default"));
994
995 let inner = sdg
996 .get("sdgs")
997 .unwrap()
998 .as_array()
999 .expect("nested sdgs should be an array");
1000 assert_eq!(inner.len(), 1);
1001
1002 let sd = &inner.first().unwrap();
1003 assert_eq!(
1004 sd.get("si").unwrap().as_str(),
1005 Some("power_requirement_max")
1006 );
1007 assert_eq!(sd.get("value").unwrap().as_str(), Some("1.21GW"));
1008}
1009
[docs]1010/// [[ itest~sovd-api-component-data-sdgsd, Data-level SDG retrieval, itest ]]
1011#[tokio::test]
1012async fn test_data_sdg_retrieval() {
1013 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
1014 let auth = auth_header(&runtime.config, None).await.unwrap();
1015
1016 let params = QueryParams(HashMap::from_iter([(
1017 "x-sovd2uds-includesdgs".to_string(),
1018 "true".to_string(),
1019 )]));
1020 let response = send_cda_request(
1021 &runtime.config,
1022 &format!(
1023 "{}/data/FluxCapacitorPowerConsumption",
1024 sovd::ECU_FLXC1000_ENDPOINT
1025 ),
1026 StatusCode::OK,
1027 Method::GET,
1028 None,
1029 Some(&auth),
1030 Some(¶ms),
1031 )
1032 .await
1033 .expect("Failed to get data SDGs");
1034
1035 let data = response_to_json(&response).unwrap();
1036
1037 // The response should be a ServicesSdgs with an "items" map
1038 let items = data
1039 .get("items")
1040 .expect("response should contain 'items'")
1041 .as_object()
1042 .expect("items should be an object");
1043
1044 assert!(
1045 !items.is_empty(),
1046 "items map should contain at least one entry"
1047 );
1048
1049 // Find the entry - key format is "{service_name}_{action:?}" lowercased
1050 let entry = items
1051 .values()
1052 .next()
1053 .expect("should have at least one service SDG entry");
1054
1055 let sdgs = entry
1056 .get("sdgs")
1057 .expect("entry should have 'sdgs'")
1058 .as_array()
1059 .expect("sdgs should be an array");
1060
1061 assert!(!sdgs.is_empty(), "sdgs array should not be empty");
1062
1063 let sdg = sdgs
1064 .first()
1065 .expect("sdgs array should have at least one element");
1066 assert_eq!(
1067 sdg.get("caption").unwrap().as_str(),
1068 Some("flux_capacitor_sdg")
1069 );
1070 assert_eq!(sdg.get("si").unwrap().as_str(), Some("sensor_metadata"));
1071
1072 let inner = sdg
1073 .get("sdgs")
1074 .unwrap()
1075 .as_array()
1076 .expect("nested sdgs should be an array");
1077 assert_eq!(inner.len(), 1);
1078
1079 let sd = inner
1080 .first()
1081 .expect("inner sdgs should have at least one element");
1082 assert_eq!(sd.get("si").unwrap().as_str(), Some("measurement_unit"));
1083 assert_eq!(sd.get("value").unwrap().as_str(), Some("gigawatts"));
1084}
1085
[docs]1086/// [[ itest~sovd-api-component-operations-sdgsd, Operation-level SDG retrieval, itest ]]
1087#[tokio::test]
1088async fn test_operation_sdg_retrieval() {
1089 let (runtime, _lock) = setup_integration_test(true).await.unwrap();
1090 let auth = auth_header(&runtime.config, None).await.unwrap();
1091
1092 let params = QueryParams(HashMap::from_iter([(
1093 "x-sovd2uds-includesdgs".to_string(),
1094 "true".to_string(),
1095 )]));
1096 let response = send_cda_request(
1097 &runtime.config,
1098 &format!("{}/operations/SelfTest", sovd::ECU_FLXC1000_ENDPOINT),
1099 StatusCode::OK,
1100 Method::GET,
1101 None,
1102 Some(&auth),
1103 Some(¶ms),
1104 )
1105 .await
1106 .expect("Failed to get operation SDGs");
1107
1108 let data = response_to_json(&response).unwrap();
1109
1110 // The response should be a ServicesSdgs with an "items" map
1111 let items = data
1112 .get("items")
1113 .expect("response should contain 'items'")
1114 .as_object()
1115 .expect("items should be an object");
1116
1117 assert!(
1118 !items.is_empty(),
1119 "items map should contain at least one entry"
1120 );
1121
1122 // Find the entry
1123 let entry = items
1124 .values()
1125 .next()
1126 .expect("should have at least one service SDG entry");
1127
1128 let sdgs = entry
1129 .get("sdgs")
1130 .expect("entry should have 'sdgs'")
1131 .as_array()
1132 .expect("sdgs should be an array");
1133
1134 assert!(!sdgs.is_empty(), "sdgs array should not be empty");
1135
1136 let sdg = sdgs
1137 .first()
1138 .expect("sdgs array should have at least one element");
1139 assert_eq!(sdg.get("caption").unwrap().as_str(), Some("self_test_sdg"));
1140 assert_eq!(sdg.get("si").unwrap().as_str(), Some("routine_metadata"));
1141
1142 let inner = sdg
1143 .get("sdgs")
1144 .unwrap()
1145 .as_array()
1146 .expect("nested sdgs should be an array");
1147 assert_eq!(inner.len(), 1);
1148
1149 let sd = inner
1150 .first()
1151 .expect("inner sdgs should have at least one element");
1152 assert_eq!(sd.get("si").unwrap().as_str(), Some("expected_duration_ms"));
1153 assert_eq!(sd.get("value").unwrap().as_str(), Some("5000"));
1154}
1155
1156async fn validate_ecu_state(
1157 runtime: &TestRuntime,
1158 auth: &HeaderMap,
1159 ecu: &str,
1160 expected_state: sovd_interfaces::components::ecu::State,
1161) {
1162 let ecu_status = ecu_status(&runtime.config, auth, ecu)
1163 .await
1164 .expect("failed to get ecu status");
1165 assert_eq!(
1166 ecu_status.variant.state, expected_state,
1167 "ECU {ecu} state does not match {ecu_status:?}"
1168 );
1169}
1170
1171async fn session(
1172 config: &Configuration,
1173 headers: &HeaderMap,
1174 ecu_endpoint: &str,
1175) -> Result<
1176 sovd_interfaces::components::ecu::modes::security_and_session::get::Response,
1177 TestingError,
1178> {
1179 get_mode(config, headers, ecu_endpoint, "session").await
1180}
1181
1182async fn security(
1183 config: &Configuration,
1184 headers: &HeaderMap,
1185 ecu_endpoint: &str,
1186) -> Result<
1187 sovd_interfaces::components::ecu::modes::security_and_session::get::Response,
1188 TestingError,
1189> {
1190 get_mode(config, headers, ecu_endpoint, "security").await
1191}
1192
1193pub(crate) async fn switch_session(
1194 name: &str,
1195 config: &Configuration,
1196 headers: &HeaderMap,
1197 ecu_endpoint: &str,
1198 expected_status: StatusCode,
1199) -> Result<
1200 Option<sovd_interfaces::components::ecu::modes::security_and_session::put::Response<String>>,
1201 TestingError,
1202> {
1203 put_mode(
1204 config,
1205 headers,
1206 ecu_endpoint,
1207 "session",
1208 sovd_interfaces::components::ecu::modes::security_and_session::put::Request {
1209 value: name.to_owned(),
1210 mode_expiration: None,
1211 key: None,
1212 },
1213 expected_status,
1214 )
1215 .await
1216}
1217
1218async fn request_seed(
1219 name: String,
1220 config: &Configuration,
1221 headers: &HeaderMap,
1222 ecu_endpoint: &str,
1223) -> Result<
1224 Option<sovd_interfaces::components::ecu::modes::security_and_session::put::RequestSeedResponse>,
1225 TestingError,
1226> {
1227 put_mode(
1228 config,
1229 headers,
1230 ecu_endpoint,
1231 "security",
1232 sovd_interfaces::components::ecu::modes::security_and_session::put::Request {
1233 value: name,
1234 mode_expiration: None,
1235 key: None,
1236 },
1237 StatusCode::OK,
1238 )
1239 .await
1240}
1241
1242async fn send_key(
1243 name: String,
1244 key: String,
1245 config: &Configuration,
1246 headers: &HeaderMap,
1247 ecu_endpoint: &str,
1248 excepted_status: StatusCode,
1249) -> Result<
1250 Option<sovd_interfaces::components::ecu::modes::security_and_session::put::Response<String>>,
1251 TestingError,
1252> {
1253 put_mode(
1254 config,
1255 headers,
1256 ecu_endpoint,
1257 "security",
1258 sovd_interfaces::components::ecu::modes::security_and_session::put::Request {
1259 value: name,
1260 mode_expiration: None,
1261 key: Some(
1262 sovd_interfaces::components::ecu::modes::security_and_session::put::ModeKey {
1263 send_key: key,
1264 },
1265 ),
1266 },
1267 excepted_status,
1268 )
1269 .await
1270}
1271
1272async fn get_mode<T: DeserializeOwned>(
1273 config: &Configuration,
1274 headers: &HeaderMap,
1275 ecu_endpoint: &str,
1276 sub_path: &str,
1277) -> Result<T, TestingError> {
1278 let http_response = send_cda_request(
1279 config,
1280 &format!("{ecu_endpoint}/modes/{sub_path}"),
1281 StatusCode::OK,
1282 Method::GET,
1283 None,
1284 Some(headers),
1285 None,
1286 )
1287 .await?;
1288 response_to_t(&http_response)
1289}
1290
1291async fn ecu_status(
1292 config: &Configuration,
1293 headers: &HeaderMap,
1294 ecu_endpoint: &str,
1295) -> Result<sovd_interfaces::components::ecu::get::Response, TestingError> {
1296 let http_response = send_cda_request(
1297 config,
1298 ecu_endpoint,
1299 StatusCode::OK,
1300 Method::GET,
1301 None,
1302 Some(headers),
1303 None,
1304 )
1305 .await?;
1306 response_to_t(&http_response)
1307}
1308
1309async fn force_variant_detection(
1310 config: &Configuration,
1311 headers: &HeaderMap,
1312 ecu_endpoint: &str,
1313) -> Result<(), TestingError> {
1314 send_cda_request(
1315 config,
1316 ecu_endpoint,
1317 StatusCode::CREATED,
1318 Method::PUT,
1319 None,
1320 Some(headers),
1321 None,
1322 )
1323 .await?;
1324 Ok(())
1325}
1326
1327async fn get_comm_control(
1328 config: &Configuration,
1329 headers: &HeaderMap,
1330 ecu_endpoint: &str,
1331) -> Result<modes::commctrl::get::Response, TestingError> {
1332 get_mode(config, headers, ecu_endpoint, "commctrl").await
1333}
1334
1335async fn set_comm_control(
1336 value: &str,
1337 parameters: Option<cda_interfaces::HashMap<String, serde_json::Value>>,
1338 config: &Configuration,
1339 headers: &HeaderMap,
1340 ecu_endpoint: &str,
1341 expected_status: StatusCode,
1342) -> Result<Option<sovd_interfaces::components::ecu::modes::commctrl::put::Response>, TestingError>
1343{
1344 put_mode(
1345 config,
1346 headers,
1347 ecu_endpoint,
1348 "commctrl",
1349 modes::commctrl::put::Request {
1350 value: value.to_owned(),
1351 parameters,
1352 },
1353 expected_status,
1354 )
1355 .await
1356}
1357
1358pub(crate) async fn get_dtc_setting(
1359 config: &Configuration,
1360 headers: &HeaderMap,
1361 ecu_endpoint: &str,
1362) -> Result<dtcsetting::get::Response, TestingError> {
1363 get_mode(config, headers, ecu_endpoint, "dtcsetting").await
1364}
1365
1366async fn get_configurations(
1367 config: &Configuration,
1368 headers: &HeaderMap,
1369 ecu_endpoint: &str,
1370 service: &str,
1371) -> Result<sovd_interfaces::components::ecu::configurations::ServiceResponse, TestingError> {
1372 let http_response = send_cda_request(
1373 config,
1374 &format!("{ecu_endpoint}/configurations/{service}"),
1375 StatusCode::OK,
1376 Method::GET,
1377 None,
1378 Some(headers),
1379 None,
1380 )
1381 .await?;
1382 response_to_t(&http_response)
1383}
1384
1385async fn reset_ecu(
1386 value: &str,
1387 config: &Configuration,
1388 headers: &HeaderMap,
1389 ecu_endpoint: &str,
1390 expected_status: StatusCode,
1391) -> Result<(), TestingError> {
1392 let body = serde_json::json!({
1393 "parameters": {"value": value}
1394 })
1395 .to_string();
1396 send_cda_request(
1397 config,
1398 &format!("{ecu_endpoint}/operations/reset/executions"),
1399 expected_status,
1400 Method::POST,
1401 Some(&body),
1402 Some(headers),
1403 None,
1404 )
1405 .await?;
1406 Ok(())
1407}
1408
1409async fn get_data_services(
1410 config: &Configuration,
1411 headers: &HeaderMap,
1412 ecu_endpoint: &str,
1413) -> Result<sovd_interfaces::components::ecu::data::get::Response, TestingError> {
1414 let http_response = send_cda_request(
1415 config,
1416 &format!("{ecu_endpoint}/data"),
1417 StatusCode::OK,
1418 Method::GET,
1419 None,
1420 Some(headers),
1421 None,
1422 )
1423 .await?;
1424 response_to_t(&http_response)
1425}