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