1#![cfg_attr(target_arch = "wasm32", allow(clippy::future_not_send))]
5
6use async_trait::async_trait;
7use uuid::Uuid;
8
9use crate::config::{NetworkConfig, RetryPolicy};
10use crate::constants::DEFAULT_RETRY_AFTER_SECONDS;
11use crate::error::HttpError;
12use crate::http::{HttpClient, execute_with_retry};
13use crate::types::{CredentialPolicyRequest, CredentialRequestResponse, RequestId, RequestStatus};
14use crate::verification::VerifiablePresentation;
15
16#[cfg(not(target_arch = "wasm32"))]
24#[async_trait]
25pub trait IdpClient: Send + Sync {
26 async fn request_credential(
37 &self,
38 policy: CredentialPolicyRequest,
39 bearer_token: &str,
40 dpop_proof: &str,
41 ) -> Result<CredentialRequestResponse, HttpError>;
42
43 async fn poll_status(
50 &self,
51 request_id: &RequestId,
52 client_id: &str,
53 ) -> Result<RequestStatus, HttpError>;
54
55 async fn get_presentation(
62 &self,
63 request_id: &RequestId,
64 client_id: &str,
65 ) -> Result<VerifiablePresentation, HttpError>;
66}
67
68#[cfg(target_arch = "wasm32")]
70#[async_trait(?Send)]
71pub trait IdpClient {
72 async fn request_credential(
91 &self,
92 policy: CredentialPolicyRequest,
93 bearer_token: &str,
94 dpop_proof: &str,
95 ) -> Result<CredentialRequestResponse, HttpError>;
96
97 async fn poll_status(
109 &self,
110 request_id: &RequestId,
111 client_id: &str,
112 ) -> Result<RequestStatus, HttpError>;
113
114 async fn get_presentation(
126 &self,
127 request_id: &RequestId,
128 client_id: &str,
129 ) -> Result<VerifiablePresentation, HttpError>;
130}
131
132#[derive(Debug, Clone)]
134pub struct CorrelationIdConfig {
135 pub enabled: bool,
137 pub header_name: String,
139}
140
141impl Default for CorrelationIdConfig {
142 fn default() -> Self {
143 Self {
144 enabled: true,
145 header_name: crate::constants::DEFAULT_CORRELATION_ID_HEADER.to_string(),
146 }
147 }
148}
149
150pub struct HttpIdpClient {
152 http_client: HttpClient,
153 base_url: String,
154 retry_policy: RetryPolicy,
155 correlation_config: CorrelationIdConfig,
156}
157
158impl HttpIdpClient {
159 pub fn new(
176 base_url: String,
177 timeout_seconds: u64,
178 retry_policy: RetryPolicy,
179 ) -> Result<Self, HttpError> {
180 let network_config = NetworkConfig {
181 request_timeout_seconds: timeout_seconds,
182 ..Default::default()
183 };
184 Self::with_network_config(base_url, &network_config, retry_policy)
185 }
186
187 pub fn with_network_config(
228 base_url: String,
229 network_config: &NetworkConfig,
230 retry_policy: RetryPolicy,
231 ) -> Result<Self, HttpError> {
232 let correlation_config = CorrelationIdConfig {
233 enabled: network_config.enable_correlation_ids,
234 header_name: network_config.correlation_id_header.clone(),
235 };
236
237 let http_client = HttpClient::new(network_config)?;
238
239 Ok(Self {
240 http_client,
241 base_url,
242 retry_policy,
243 correlation_config,
244 })
245 }
246
247 fn generate_correlation_id(&self) -> Option<String> {
251 if self.correlation_config.enabled {
252 Some(Uuid::new_v4().to_string())
253 } else {
254 None
255 }
256 }
257
258 async fn response_to_error(response: reqwest::Response) -> HttpError {
260 let status = response.status().as_u16();
261
262 if status == 429 {
264 let retry_after = response
265 .headers()
266 .get("Retry-After")
267 .and_then(|v| v.to_str().ok())
268 .and_then(|s| s.parse().ok())
269 .unwrap_or(DEFAULT_RETRY_AFTER_SECONDS);
270
271 return HttpError::rate_limited(retry_after);
272 }
273
274 let message = response.text().await.unwrap_or_default();
275 HttpError::status(status, message)
276 }
277}
278
279fn is_issuer_jwt(part: &str) -> bool {
285 use base64::Engine;
286 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
287
288 if !part.contains('.') {
290 return false;
291 }
292
293 let header_b64 = match part.split('.').next() {
295 Some(h) if h.starts_with("eyJ") => h,
296 _ => return false,
297 };
298
299 if let Ok(header_bytes) = URL_SAFE_NO_PAD.decode(header_b64)
301 && let Ok(header) = serde_json::from_slice::<serde_json::Value>(&header_bytes)
302 {
303 return header.get("typ").and_then(|v| v.as_str()) == Some("vc+sd-jwt");
304 }
305
306 false
307}
308
309fn split_vp_tokens(presentation: &str) -> Vec<String> {
322 if presentation.is_empty() {
323 return Vec::new();
324 }
325
326 let parts: Vec<&str> = presentation.split('~').collect();
327 let mut tokens: Vec<String> = Vec::new();
328 let mut current_token = String::new();
329
330 for part in parts {
331 if is_issuer_jwt(part) && !current_token.is_empty() {
333 tokens.push(current_token);
335 current_token = part.to_string();
336 } else {
337 if !current_token.is_empty() {
339 current_token.push('~');
340 }
341 current_token.push_str(part);
342 }
343 }
344
345 if !current_token.is_empty() {
346 tokens.push(current_token);
347 }
348
349 tokens
350}
351
352fn build_verification_request(policy: &CredentialPolicyRequest) -> serde_json::Value {
388 use serde_json::json;
389
390 let credential_types = &policy.credential_types;
392
393 let vct_filter = if credential_types.len() == 1 {
396 json!({
397 "type": "string",
398 "const": credential_types[0]
399 })
400 } else {
401 json!({
402 "type": "string",
403 "enum": credential_types
404 })
405 };
406
407 let mut fields = vec![json!({
409 "path": ["$.vct"],
410 "filter": vct_filter
411 })];
412
413 if let Some(required) = &policy.required_fields {
415 for field in required {
416 fields.push(json!({
417 "path": [format!("$.{}", field)]
418 }));
419 }
420 }
421
422 if let Some(optional) = &policy.optional_fields {
424 for field in optional {
425 fields.push(json!({
426 "path": [format!("$.{}", field)],
427 "optional": true
428 }));
429 }
430 }
431
432 if let Some(constraints) = &policy.constraints {
434 for (key, value) in constraints {
435 if key == "requestedFields" || key == "optionalFields" {
437 continue;
438 }
439 fields.push(json!({
440 "path": [format!("$.{}", key)],
441 "filter": value
442 }));
443 }
444 }
445
446 let mut request = json!({
447 "client_id": &policy.client_id,
448 "response_type": "vp_token",
449 "response_mode": "direct_post",
450 "presentation_definition": {
451 "id": "sdk-request",
452 "input_descriptors": [{
453 "id": "credential",
454 "constraints": {
455 "fields": fields
456 }
457 }]
458 }
459 });
460
461 if let Some(callback) = &policy.callback_url {
466 request["redirect_uri"] = json!(callback);
467 }
468
469 if let Some(state) = &policy.state {
471 request["state"] = json!(state);
472 }
473
474 if let Some(nonce) = &policy.nonce {
476 request["nonce"] = json!(nonce);
477 }
478
479 request
480}
481
482#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
483#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
484impl IdpClient for HttpIdpClient {
485 async fn request_credential(
486 &self,
487 policy: CredentialPolicyRequest,
488 bearer_token: &str,
489 dpop_proof: &str,
490 ) -> Result<CredentialRequestResponse, HttpError> {
491 let url = format!(
496 "{}/v1/verify/request?client_id={}",
497 self.base_url,
498 urlencoding::encode(&policy.client_id)
499 );
500 let token = bearer_token.to_string();
501 let proof = dpop_proof.to_string();
502 let policy_clone = policy.clone();
503
504 let correlation_id = self.generate_correlation_id();
506 let correlation_header = self.correlation_config.header_name.clone();
507
508 #[cfg(feature = "tracing")]
509 if let Some(ref cid) = correlation_id {
510 tracing::debug!(correlation_id = %cid, "Generated correlation ID for credential request");
511 }
512
513 execute_with_retry(&self.retry_policy, || {
514 let url = url.clone();
515 let token = token.clone();
516 let proof = proof.clone();
517 let policy = policy_clone.clone();
518 let client = &self.http_client;
519 let correlation_id = correlation_id.clone();
520 let correlation_header = correlation_header.clone();
521
522 async move {
523 let idp_request = build_verification_request(&policy);
525
526 let mut request_builder = client
528 .post(&url)
529 .header("Authorization", format!("DPoP {token}"))
530 .header("DPoP", &proof);
531
532 if let Some(ref cid) = correlation_id {
534 request_builder = request_builder.header(&correlation_header, cid);
535 }
536
537 let response = request_builder
538 .json(&idp_request)
539 .send()
540 .await
541 .map_err(|e| {
542 if e.is_timeout() {
543 HttpError::Timeout
544 } else {
545 HttpError::network(e.to_string())
546 }
547 })?;
548
549 if response.status().is_success() {
550 #[derive(serde::Deserialize)]
552 struct IdpResponse {
553 request_id: String,
554 request_uri: String,
555 expires_in: u64,
556 nonce: String,
557 }
558
559 let body: IdpResponse = response
560 .json()
561 .await
562 .map_err(|e| HttpError::network(e.to_string()))?;
563
564 if !body.request_uri.starts_with("openid4vp://")
566 && !body.request_uri.starts_with("openid://")
567 {
568 return Err(HttpError::network(format!(
569 "Invalid request_uri scheme: expected openid4vp:// or openid://, got {}",
570 body.request_uri.split("://").next().unwrap_or("unknown")
571 )));
572 }
573
574 Ok(CredentialRequestResponse::new(
575 RequestId::new(body.request_id),
576 body.request_uri,
577 body.expires_in,
578 body.nonce,
579 ))
580 } else {
581 Err(Self::response_to_error(response).await)
582 }
583 }
584 })
585 .await
586 }
587
588 async fn poll_status(
589 &self,
590 request_id: &RequestId,
591 client_id: &str,
592 ) -> Result<RequestStatus, HttpError> {
593 let url = format!(
598 "{}/v1/verify/status/{}?client_id={}",
599 self.base_url,
600 request_id.as_str(),
601 urlencoding::encode(client_id)
602 );
603 let rid = request_id.clone();
604
605 let correlation_id = self.generate_correlation_id();
607 let correlation_header = self.correlation_config.header_name.clone();
608
609 #[cfg(feature = "tracing")]
610 if let Some(ref cid) = correlation_id {
611 tracing::debug!(correlation_id = %cid, request_id = %request_id.as_str(), "Generated correlation ID for status poll");
612 }
613
614 execute_with_retry(&self.retry_policy, || {
615 let url = url.clone();
616 let client = &self.http_client;
617 let request_id = rid.clone();
618 let correlation_id = correlation_id.clone();
619 let correlation_header = correlation_header.clone();
620
621 async move {
622 let mut request_builder = client.get(&url);
623
624 if let Some(ref cid) = correlation_id {
626 request_builder = request_builder.header(&correlation_header, cid);
627 }
628
629 let response = request_builder.send().await.map_err(|e| {
630 if e.is_timeout() {
631 HttpError::Timeout
632 } else {
633 HttpError::network(e.to_string())
634 }
635 })?;
636
637 if response.status().is_success() {
638 #[derive(serde::Deserialize)]
641 #[allow(dead_code)]
642 struct IdpStatus {
643 request_id: String,
644 state: String,
645 #[serde(default)]
648 vp_token: Option<Vec<String>>,
649 expires_at: Option<String>,
650 }
651
652 let body: IdpStatus = response
653 .json()
654 .await
655 .map_err(|e| HttpError::network(e.to_string()))?;
656
657 let status = match body.state.as_str() {
659 "verified" => {
660 let presentation = body
663 .vp_token
664 .map(|tokens| tokens.join("~"))
665 .unwrap_or_default();
666 RequestStatus::Fulfilled {
667 request_id,
668 presentation,
669 fulfilled_at: body.expires_at.unwrap_or_default(),
670 }
671 }
672 "denied" | "failed" => RequestStatus::Denied {
673 request_id,
674 reason: "User declined or verification failed".to_string(),
675 denied_at: body.expires_at.unwrap_or_default(),
676 },
677 "expired" => RequestStatus::Expired {
678 request_id,
679 expired_at: body.expires_at.unwrap_or_default(),
680 },
681 _ => RequestStatus::Pending {
683 request_id,
684 created_at: body.expires_at.unwrap_or_default(),
685 },
686 };
687 Ok(status)
688 } else if response.status().as_u16() == 410 {
689 Ok(RequestStatus::Expired {
691 request_id,
692 expired_at: String::new(),
693 })
694 } else if response.status().as_u16() == 404 {
695 let message = response.text().await.unwrap_or_default();
697 Ok(RequestStatus::NotFound {
698 request_id,
699 message,
700 })
701 } else {
702 Err(Self::response_to_error(response).await)
703 }
704 }
705 })
706 .await
707 }
708
709 async fn get_presentation(
710 &self,
711 request_id: &RequestId,
712 client_id: &str,
713 ) -> Result<VerifiablePresentation, HttpError> {
714 let status = self.poll_status(request_id, client_id).await?;
716
717 match status {
718 RequestStatus::Fulfilled { presentation, .. } => {
719 let first_token = split_vp_tokens(&presentation)
724 .into_iter()
725 .next()
726 .unwrap_or(presentation);
727 VerifiablePresentation::parse(&first_token)
728 .map_err(|e| HttpError::network(format!("Failed to parse presentation: {e}")))
729 }
730 RequestStatus::Pending { .. } => {
731 Err(HttpError::status(404, "Request not yet fulfilled"))
732 }
733 RequestStatus::Denied { reason, .. } => Err(HttpError::status(403, reason)),
734 RequestStatus::Expired { .. } => Err(HttpError::status(410, "Request expired")),
735 RequestStatus::NotFound { message, .. } => Err(HttpError::status(404, message)),
736 RequestStatus::Timeout { .. } => Err(HttpError::status(408, "Request timed out")),
737 }
738 }
739}
740
741impl Clone for HttpIdpClient {
742 fn clone(&self) -> Self {
743 Self {
744 http_client: self.http_client.clone(),
745 base_url: self.base_url.clone(),
746 retry_policy: self.retry_policy.clone(),
747 correlation_config: self.correlation_config.clone(),
748 }
749 }
750}
751
752#[cfg(any(test, feature = "test-utils"))]
753pub mod mock {
754 use std::collections::VecDeque;
757 use std::sync::Mutex;
758
759 use super::{
760 CredentialPolicyRequest, CredentialRequestResponse, HttpError, IdpClient, RequestId,
761 RequestStatus, VerifiablePresentation, async_trait,
762 };
763
764 pub struct MockIdpClient {
766 requests: Mutex<VecDeque<Result<CredentialRequestResponse, HttpError>>>,
767 polls: Mutex<VecDeque<Result<RequestStatus, HttpError>>>,
768 presentations: Mutex<VecDeque<Result<VerifiablePresentation, HttpError>>>,
769 }
770
771 impl MockIdpClient {
772 #[must_use]
774 #[allow(clippy::missing_const_for_fn)] pub fn new() -> Self {
776 Self {
777 requests: Mutex::new(VecDeque::new()),
778 polls: Mutex::new(VecDeque::new()),
779 presentations: Mutex::new(VecDeque::new()),
780 }
781 }
782
783 pub fn expect_request(&self, response: Result<CredentialRequestResponse, HttpError>) {
785 self.requests.lock().unwrap().push_back(response);
786 }
787
788 pub fn expect_poll(&self, response: Result<RequestStatus, HttpError>) {
790 self.polls.lock().unwrap().push_back(response);
791 }
792
793 pub fn expect_presentation(&self, response: Result<VerifiablePresentation, HttpError>) {
795 self.presentations.lock().unwrap().push_back(response);
796 }
797 }
798
799 impl Default for MockIdpClient {
800 fn default() -> Self {
801 Self::new()
802 }
803 }
804
805 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
806 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
807 impl IdpClient for MockIdpClient {
808 async fn request_credential(
809 &self,
810 _policy: CredentialPolicyRequest,
811 _bearer_token: &str,
812 _dpop_proof: &str,
813 ) -> Result<CredentialRequestResponse, HttpError> {
814 self.requests
815 .lock()
816 .unwrap()
817 .pop_front()
818 .unwrap_or_else(|| Err(HttpError::network("Mock not configured")))
819 }
820
821 async fn poll_status(
822 &self,
823 _request_id: &RequestId,
824 _client_id: &str,
825 ) -> Result<RequestStatus, HttpError> {
826 self.polls
827 .lock()
828 .unwrap()
829 .pop_front()
830 .unwrap_or_else(|| Err(HttpError::network("Mock not configured")))
831 }
832
833 async fn get_presentation(
834 &self,
835 _request_id: &RequestId,
836 _client_id: &str,
837 ) -> Result<VerifiablePresentation, HttpError> {
838 self.presentations
839 .lock()
840 .unwrap()
841 .pop_front()
842 .unwrap_or_else(|| Err(HttpError::network("Mock not configured")))
843 }
844 }
845}
846
847#[cfg(test)]
848mod tests {
849 use super::*;
850 use crate::types::credential_chain::types;
851
852 #[test]
853 fn test_build_verification_request_base_uses_const() {
854 let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
856 policy.client_id = "test-client".to_string();
857
858 let request = build_verification_request(&policy);
859
860 assert_eq!(request["response_type"], "vp_token");
862 assert_eq!(request["response_mode"], "direct_post");
863 assert_eq!(request["client_id"], "test-client");
864
865 let descriptors = &request["presentation_definition"]["input_descriptors"];
867 assert!(descriptors.is_array());
868 assert_eq!(descriptors.as_array().unwrap().len(), 1);
869 assert_eq!(descriptors[0]["id"], "credential");
870
871 let filter = &descriptors[0]["constraints"]["fields"][0]["filter"];
873 assert_eq!(filter["type"], "string");
874 assert_eq!(filter["const"], types::BASE);
875 assert!(
876 filter["enum"].is_null(),
877 "Single type should use const, not enum"
878 );
879 }
880
881 #[test]
882 fn test_build_verification_request_verified_email_uses_enum() {
883 let mut policy = CredentialPolicyRequest::with_chain(types::VERIFIED_EMAIL);
885 policy.client_id = "test-client".to_string();
886
887 let request = build_verification_request(&policy);
888
889 let descriptors = &request["presentation_definition"]["input_descriptors"];
891 let descriptors_arr = descriptors.as_array().unwrap();
892 assert_eq!(descriptors_arr.len(), 1);
893 assert_eq!(descriptors_arr[0]["id"], "credential");
894
895 let filter = &descriptors_arr[0]["constraints"]["fields"][0]["filter"];
897 assert_eq!(filter["type"], "string");
898 let enum_values = filter["enum"].as_array().expect("Should have enum filter");
899 assert_eq!(enum_values.len(), 2);
900 assert_eq!(enum_values[0], types::BASE);
901 assert_eq!(enum_values[1], types::VERIFIED_EMAIL);
902 }
903
904 #[test]
905 fn test_build_verification_request_employment_uses_enum() {
906 let mut policy = CredentialPolicyRequest::with_chain(types::EMPLOYMENT);
908 policy.client_id = "test-client".to_string();
909
910 let request = build_verification_request(&policy);
911
912 let descriptors = &request["presentation_definition"]["input_descriptors"];
914 let descriptors_arr = descriptors.as_array().unwrap();
915 assert_eq!(descriptors_arr.len(), 1);
916
917 let filter = &descriptors_arr[0]["constraints"]["fields"][0]["filter"];
919 let enum_values = filter["enum"].as_array().expect("Should have enum filter");
920 assert_eq!(enum_values.len(), 3);
921 assert_eq!(enum_values[0], types::BASE);
922 assert_eq!(enum_values[1], types::PERSONA);
923 assert_eq!(enum_values[2], types::EMPLOYMENT);
924 }
925
926 #[test]
927 fn test_build_verification_request_with_constraints() {
928 let mut policy = CredentialPolicyRequest::with_chain(types::VERIFIED_EMAIL);
930 policy.client_id = "test-client".to_string();
931 policy.add_constraint("domain", serde_json::json!({"pattern": ".*@example.com"}));
932
933 let request = build_verification_request(&policy);
934
935 let descriptors = &request["presentation_definition"]["input_descriptors"];
937 let fields = descriptors[0]["constraints"]["fields"].as_array().unwrap();
938 assert_eq!(fields.len(), 2);
939
940 assert_eq!(fields[0]["path"][0], "$.vct");
942 assert!(fields[0]["filter"]["enum"].is_array());
943
944 assert_eq!(fields[1]["path"][0], "$.domain");
946 assert_eq!(fields[1]["filter"]["pattern"], ".*@example.com");
947 }
948
949 #[test]
950 fn test_build_verification_request_no_extra_fields() {
951 let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
952 policy.client_id = "test-client".to_string();
953
954 let request = build_verification_request(&policy);
955
956 let descriptor = &request["presentation_definition"]["input_descriptors"][0];
957
958 assert!(
960 descriptor["format"].is_null(),
961 "Should not have 'format' field"
962 );
963 assert!(descriptor["name"].is_null(), "Should not have 'name' field");
964 assert!(
965 descriptor["purpose"].is_null(),
966 "Should not have 'purpose' field"
967 );
968
969 assert_eq!(descriptor["id"], "credential");
971 assert!(!descriptor["constraints"].is_null());
972 }
973
974 #[test]
975 fn test_build_verification_request_with_callback() {
976 let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
977 policy.client_id = "test-client".to_string();
978 policy.callback_url = Some("https://example.com/callback".to_string());
979
980 let request = build_verification_request(&policy);
981
982 assert_eq!(request["redirect_uri"], "https://example.com/callback");
983 }
984
985 #[test]
986 fn test_build_verification_request_static_id() {
987 let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
988 policy.client_id = "test-client".to_string();
989
990 let request = build_verification_request(&policy);
991
992 assert_eq!(request["presentation_definition"]["id"], "sdk-request");
994 }
995
996 #[test]
997 fn test_build_verification_request_with_state() {
998 let policy =
999 CredentialPolicyRequest::with_chain(types::BASE).with_state("csrf-token-12345");
1000
1001 let request = build_verification_request(&policy);
1002
1003 assert_eq!(request["state"], "csrf-token-12345");
1005
1006 let fields =
1008 &request["presentation_definition"]["input_descriptors"][0]["constraints"]["fields"];
1009 for field in fields.as_array().unwrap() {
1010 let path = field["path"][0].as_str().unwrap();
1011 assert_ne!(path, "$.state", "state should NOT be a field constraint");
1012 }
1013 }
1014
1015 #[test]
1016 fn test_build_verification_request_with_nonce() {
1017 let policy = CredentialPolicyRequest::with_chain(types::BASE).with_nonce("nonce-67890");
1018
1019 let request = build_verification_request(&policy);
1020
1021 assert_eq!(request["nonce"], "nonce-67890");
1023
1024 let fields =
1026 &request["presentation_definition"]["input_descriptors"][0]["constraints"]["fields"];
1027 for field in fields.as_array().unwrap() {
1028 let path = field["path"][0].as_str().unwrap();
1029 assert_ne!(path, "$.nonce", "nonce should NOT be a field constraint");
1030 }
1031 }
1032
1033 #[test]
1034 fn test_build_verification_request_with_all_openid4vp_fields() {
1035 let policy = CredentialPolicyRequest::with_chain(types::BASE)
1036 .with_callback("https://example.com/callback")
1037 .with_state("csrf-token")
1038 .with_nonce("binding-nonce");
1039
1040 let request = build_verification_request(&policy);
1041
1042 assert_eq!(request["redirect_uri"], "https://example.com/callback");
1044 assert_eq!(request["state"], "csrf-token");
1045 assert_eq!(request["nonce"], "binding-nonce");
1046 assert_eq!(request["response_type"], "vp_token");
1047 assert_eq!(request["response_mode"], "direct_post");
1048 }
1049
1050 #[test]
1051 fn test_build_verification_request_without_optional_fields() {
1052 let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
1053 policy.client_id = "test-client".to_string();
1054 let request = build_verification_request(&policy);
1057
1058 assert!(request.get("redirect_uri").is_none() || request["redirect_uri"].is_null());
1060 assert!(request.get("state").is_none() || request["state"].is_null());
1061 assert!(request.get("nonce").is_none() || request["nonce"].is_null());
1062
1063 assert_eq!(request["client_id"], "test-client");
1065 assert_eq!(request["response_type"], "vp_token");
1066 }
1067
1068 #[test]
1069 fn test_build_verification_request_with_required_and_optional_fields() {
1070 let mut policy = CredentialPolicyRequest::with_chain(types::PERSONA);
1071 policy.client_id = "test-client".to_string();
1072 policy.required_fields = Some(vec!["display_name".to_string()]);
1073 policy.optional_fields = Some(vec!["email".to_string(), "phone".to_string()]);
1074
1075 let request = build_verification_request(&policy);
1076
1077 let fields =
1078 &request["presentation_definition"]["input_descriptors"][0]["constraints"]["fields"]
1079 .as_array()
1080 .unwrap();
1081
1082 assert_eq!(fields.len(), 4);
1084
1085 assert_eq!(fields[0]["path"][0], "$.vct");
1087 assert!(fields[0]["filter"]["enum"].is_array());
1088
1089 assert_eq!(fields[1]["path"][0], "$.display_name");
1091 assert!(
1092 fields[1].get("optional").is_none() || fields[1]["optional"].is_null(),
1093 "Required field should not have optional flag"
1094 );
1095
1096 assert_eq!(fields[2]["path"][0], "$.email");
1098 assert_eq!(fields[2]["optional"], true);
1099
1100 assert_eq!(fields[3]["path"][0], "$.phone");
1102 assert_eq!(fields[3]["optional"], true);
1103 }
1104
1105 #[test]
1106 fn test_build_verification_request_skips_deprecated_requested_fields() {
1107 let mut policy = CredentialPolicyRequest::with_chain(types::PERSONA);
1108 policy.client_id = "test-client".to_string();
1109 policy.add_constraint(
1111 "requestedFields",
1112 serde_json::json!(["old_field1", "old_field2"]),
1113 );
1114 policy.required_fields = Some(vec!["display_name".to_string()]);
1116
1117 let request = build_verification_request(&policy);
1118
1119 let fields =
1120 &request["presentation_definition"]["input_descriptors"][0]["constraints"]["fields"]
1121 .as_array()
1122 .unwrap();
1123
1124 assert_eq!(fields.len(), 2);
1126 assert_eq!(fields[0]["path"][0], "$.vct");
1127 assert_eq!(fields[1]["path"][0], "$.display_name");
1128
1129 for field in fields.iter() {
1131 let path = field["path"][0].as_str().unwrap();
1132 assert_ne!(
1133 path, "$.requestedFields",
1134 "requestedFields should not appear as a constraint field"
1135 );
1136 }
1137 }
1138}