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