1use crate::errors::{AuthError, Result};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use std::sync::Arc;
21use std::time::{SystemTime, UNIX_EPOCH};
22use tokio::sync::RwLock;
23use uuid::Uuid;
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum CredentialFormat {
30 #[serde(rename = "jwt_vc_json")]
32 JwtVcJson,
33 #[serde(rename = "ldp_vc")]
35 LdpVc,
36 #[serde(rename = "vc+sd-jwt")]
38 SdJwtVc,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct IssuerMetadata {
46 pub credential_issuer: String,
48 pub credential_endpoint: String,
50 #[serde(default)]
52 pub batch_credential_endpoint: Option<String>,
53 pub credential_configurations_supported: HashMap<String, CredentialConfiguration>,
55 #[serde(default)]
57 pub display: Vec<IssuerDisplay>,
58}
59
60impl IssuerMetadata {
61 pub fn builder(issuer: impl Into<String>) -> IssuerMetadataBuilder {
79 let issuer = issuer.into();
80 let endpoint = format!("{}/credential", issuer);
81 IssuerMetadataBuilder {
82 issuer,
83 credential_endpoint: endpoint,
84 batch_credential_endpoint: None,
85 configs: HashMap::new(),
86 display: Vec::new(),
87 }
88 }
89}
90
91pub struct IssuerMetadataBuilder {
93 issuer: String,
94 credential_endpoint: String,
95 batch_credential_endpoint: Option<String>,
96 configs: HashMap<String, CredentialConfiguration>,
97 display: Vec<IssuerDisplay>,
98}
99
100impl IssuerMetadataBuilder {
101 pub fn credential_endpoint(mut self, url: impl Into<String>) -> Self {
103 self.credential_endpoint = url.into();
104 self
105 }
106
107 pub fn batch_credential_endpoint(mut self, url: impl Into<String>) -> Self {
109 self.batch_credential_endpoint = Some(url.into());
110 self
111 }
112
113 pub fn add_credential(
115 mut self,
116 id: impl Into<String>,
117 config: CredentialConfiguration,
118 ) -> Self {
119 self.configs.insert(id.into(), config);
120 self
121 }
122
123 pub fn display(mut self, name: impl Into<String>, locale: Option<&str>) -> Self {
125 self.display.push(IssuerDisplay {
126 name: name.into(),
127 locale: locale.map(String::from),
128 });
129 self
130 }
131
132 pub fn build(self) -> IssuerMetadata {
134 IssuerMetadata {
135 credential_issuer: self.issuer,
136 credential_endpoint: self.credential_endpoint,
137 batch_credential_endpoint: self.batch_credential_endpoint,
138 credential_configurations_supported: self.configs,
139 display: self.display,
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct IssuerDisplay {
147 pub name: String,
148 #[serde(default)]
149 pub locale: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct CredentialConfiguration {
155 pub format: CredentialFormat,
157 #[serde(default)]
159 pub scope: Option<String>,
160 #[serde(default)]
162 pub cryptographic_binding_methods_supported: Vec<String>,
163 #[serde(default)]
165 pub credential_signing_alg_values_supported: Vec<String>,
166 #[serde(default)]
168 pub display: Vec<CredentialDisplay>,
169 #[serde(default)]
171 pub credential_definition: Option<CredentialDefinition>,
172}
173
174impl CredentialConfiguration {
175 pub fn new(format: CredentialFormat) -> Self {
188 Self {
189 format,
190 scope: None,
191 cryptographic_binding_methods_supported: Vec::new(),
192 credential_signing_alg_values_supported: Vec::new(),
193 display: Vec::new(),
194 credential_definition: None,
195 }
196 }
197
198 pub fn scope(mut self, scope: impl Into<String>) -> Self {
200 self.scope = Some(scope.into());
201 self
202 }
203
204 pub fn binding_methods(mut self, methods: Vec<impl Into<String>>) -> Self {
206 self.cryptographic_binding_methods_supported =
207 methods.into_iter().map(Into::into).collect();
208 self
209 }
210
211 pub fn signing_algorithms(mut self, algs: Vec<impl Into<String>>) -> Self {
213 self.credential_signing_alg_values_supported = algs.into_iter().map(Into::into).collect();
214 self
215 }
216
217 pub fn with_display(mut self, display: CredentialDisplay) -> Self {
219 self.display.push(display);
220 self
221 }
222
223 pub fn with_definition(mut self, definition: CredentialDefinition) -> Self {
225 self.credential_definition = Some(definition);
226 self
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct CredentialDisplay {
233 pub name: String,
234 #[serde(default)]
235 pub locale: Option<String>,
236 #[serde(default)]
237 pub description: Option<String>,
238 #[serde(default)]
239 pub background_color: Option<String>,
240 #[serde(default)]
241 pub text_color: Option<String>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct CredentialDefinition {
247 #[serde(rename = "type")]
248 pub types: Vec<String>,
249 #[serde(default)]
250 pub credential_subject: Option<HashMap<String, ClaimMetadata>>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct ClaimMetadata {
256 #[serde(default)]
257 pub mandatory: bool,
258 #[serde(default)]
259 pub display: Vec<ClaimDisplay>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct ClaimDisplay {
265 pub name: String,
266 #[serde(default)]
267 pub locale: Option<String>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct CredentialOffer {
275 pub credential_issuer: String,
277 pub credential_configuration_ids: Vec<String>,
279 #[serde(default)]
281 pub grants: Option<CredentialOfferGrants>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct CredentialOfferGrants {
286 #[serde(default, rename = "authorization_code")]
288 pub authorization_code: Option<AuthorizationCodeGrant>,
289 #[serde(
291 default,
292 rename = "urn:ietf:params:oauth:grant-type:pre-authorized_code"
293 )]
294 pub pre_authorized_code: Option<PreAuthorizedCodeGrant>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct AuthorizationCodeGrant {
299 #[serde(default)]
300 pub issuer_state: Option<String>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct PreAuthorizedCodeGrant {
305 #[serde(rename = "pre-authorized_code")]
306 pub pre_authorized_code: String,
307 #[serde(default)]
308 pub user_pin_required: bool,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct CredentialRequest {
316 pub format: CredentialFormat,
318 #[serde(default)]
320 pub credential_definition: Option<CredentialDefinition>,
321 #[serde(default)]
323 pub proof: Option<CredentialProof>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct CredentialProof {
329 pub proof_type: String,
331 #[serde(default)]
333 pub jwt: Option<String>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct CredentialResponse {
341 #[serde(default)]
343 pub credential: Option<serde_json::Value>,
344 #[serde(default)]
346 pub transaction_id: Option<String>,
347 #[serde(default)]
349 pub c_nonce: Option<String>,
350 #[serde(default)]
352 pub c_nonce_expires_in: Option<u64>,
353}
354
355impl CredentialResponse {
356 pub fn immediate(
371 credential: serde_json::Value,
372 c_nonce: impl Into<String>,
373 c_nonce_expires_in: u64,
374 ) -> Self {
375 Self {
376 credential: Some(credential),
377 transaction_id: None,
378 c_nonce: Some(c_nonce.into()),
379 c_nonce_expires_in: Some(c_nonce_expires_in),
380 }
381 }
382
383 pub fn deferred(
394 transaction_id: impl Into<String>,
395 c_nonce: impl Into<String>,
396 c_nonce_expires_in: u64,
397 ) -> Self {
398 Self {
399 credential: None,
400 transaction_id: Some(transaction_id.into()),
401 c_nonce: Some(c_nonce.into()),
402 c_nonce_expires_in: Some(c_nonce_expires_in),
403 }
404 }
405
406 pub fn completed(credential: serde_json::Value) -> Self {
408 Self {
409 credential: Some(credential),
410 transaction_id: None,
411 c_nonce: None,
412 c_nonce_expires_in: None,
413 }
414 }
415}
416
417fn base64_url_decode(input: &str) -> Result<Vec<u8>> {
420 use base64::Engine;
421 base64::engine::general_purpose::URL_SAFE_NO_PAD
422 .decode(input)
423 .map_err(|e| AuthError::validation(format!("Base64url decode error: {e}")))
424}
425
426struct PendingIssuance {
428 request: CredentialRequest,
429 subject_id: String,
430 created_at: u64,
431}
432
433pub struct CredentialIssuer {
435 metadata: IssuerMetadata,
436 pre_auth_codes: Arc<RwLock<HashMap<String, (String, String)>>>,
438 deferred: Arc<RwLock<HashMap<String, PendingIssuance>>>,
440 nonces: Arc<RwLock<HashMap<String, u64>>>,
442}
443
444impl CredentialIssuer {
445 pub fn new(metadata: IssuerMetadata) -> Self {
447 Self {
448 metadata,
449 pre_auth_codes: Arc::new(RwLock::new(HashMap::new())),
450 deferred: Arc::new(RwLock::new(HashMap::new())),
451 nonces: Arc::new(RwLock::new(HashMap::new())),
452 }
453 }
454
455 pub fn metadata(&self) -> &IssuerMetadata {
457 &self.metadata
458 }
459
460 pub async fn generate_nonce(&self, lifetime_secs: u64) -> String {
462 let nonce = Uuid::new_v4().to_string();
463 let expires = SystemTime::now()
464 .duration_since(UNIX_EPOCH)
465 .unwrap_or_default()
466 .as_secs()
467 + lifetime_secs;
468 self.nonces.write().await.insert(nonce.clone(), expires);
469 nonce
470 }
471
472 pub async fn validate_nonce(&self, nonce: &str) -> bool {
474 let now = SystemTime::now()
475 .duration_since(UNIX_EPOCH)
476 .unwrap_or_default()
477 .as_secs();
478 let nonces = self.nonces.read().await;
479 matches!(nonces.get(nonce), Some(&exp) if exp > now)
480 }
481
482 pub async fn consume_nonce(&self, nonce: &str) -> bool {
484 let now = SystemTime::now()
485 .duration_since(UNIX_EPOCH)
486 .unwrap_or_default()
487 .as_secs();
488 let mut nonces = self.nonces.write().await;
489 match nonces.remove(nonce) {
490 Some(exp) if exp > now => true,
491 _ => false,
492 }
493 }
494
495 pub async fn create_offer(
497 &self,
498 credential_config_ids: Vec<String>,
499 subject_id: &str,
500 ) -> Result<CredentialOffer> {
501 if credential_config_ids.is_empty() {
502 return Err(AuthError::validation(
503 "At least one credential configuration ID is required",
504 ));
505 }
506
507 for id in &credential_config_ids {
509 if !self
510 .metadata
511 .credential_configurations_supported
512 .contains_key(id)
513 {
514 return Err(AuthError::validation(&format!(
515 "Unknown credential configuration: {id}"
516 )));
517 }
518 }
519
520 let code = Uuid::new_v4().to_string();
521 self.pre_auth_codes.write().await.insert(
522 code.clone(),
523 (credential_config_ids[0].clone(), subject_id.to_string()),
524 );
525
526 Ok(CredentialOffer {
527 credential_issuer: self.metadata.credential_issuer.clone(),
528 credential_configuration_ids: credential_config_ids,
529 grants: Some(CredentialOfferGrants {
530 authorization_code: None,
531 pre_authorized_code: Some(PreAuthorizedCodeGrant {
532 pre_authorized_code: code,
533 user_pin_required: false,
534 }),
535 }),
536 })
537 }
538
539 pub async fn validate_pre_auth_code(&self, code: &str) -> Result<(String, String)> {
541 self.pre_auth_codes
542 .write()
543 .await
544 .remove(code)
545 .ok_or_else(|| AuthError::validation("Invalid or expired pre-authorized code"))
546 }
547
548 pub async fn issue_credential(
553 &self,
554 request: &CredentialRequest,
555 subject_id: &str,
556 credential_data: Option<serde_json::Value>,
557 ) -> Result<CredentialResponse> {
558 let supported = self
560 .metadata
561 .credential_configurations_supported
562 .values()
563 .any(|c| c.format == request.format);
564 if !supported {
565 return Err(AuthError::validation(&format!(
566 "Unsupported credential format: {:?}",
567 request.format
568 )));
569 }
570
571 if let Some(ref proof) = request.proof {
573 self.validate_proof(proof).await?;
574 }
575
576 let c_nonce = self.generate_nonce(300).await;
578
579 match credential_data {
580 Some(data) => Ok(CredentialResponse::immediate(data, c_nonce, 300)),
581 None => {
582 let tx_id = Uuid::new_v4().to_string();
584 let now = SystemTime::now()
585 .duration_since(UNIX_EPOCH)
586 .unwrap_or_default()
587 .as_secs();
588 self.deferred.write().await.insert(
589 tx_id.clone(),
590 PendingIssuance {
591 request: request.clone(),
592 subject_id: subject_id.to_string(),
593 created_at: now,
594 },
595 );
596 Ok(CredentialResponse::deferred(tx_id, c_nonce, 300))
597 }
598 }
599 }
600
601 async fn validate_proof(&self, proof: &CredentialProof) -> Result<()> {
608 if proof.proof_type != "jwt" {
609 return Err(AuthError::validation(&format!(
610 "Unsupported proof type: {}",
611 proof.proof_type
612 )));
613 }
614 let jwt = proof
615 .jwt
616 .as_deref()
617 .ok_or_else(|| AuthError::validation("JWT proof value is missing"))?;
618
619 let parts: Vec<&str> = jwt.split('.').collect();
621 if parts.len() != 3 {
622 return Err(AuthError::validation(
623 "Proof JWT must be compact JWS (header.payload.signature)",
624 ));
625 }
626
627 let payload_bytes = base64_url_decode(parts[1])?;
629 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes)
630 .map_err(|e| AuthError::validation(format!("Invalid proof JWT payload: {e}")))?;
631
632 if let Some(nonce) = payload.get("nonce").and_then(|v| v.as_str()) {
633 if !self.consume_nonce(nonce).await {
634 return Err(AuthError::validation(
635 "Proof JWT nonce is invalid or expired",
636 ));
637 }
638 }
639
640 Ok(())
641 }
642
643 pub async fn issue_batch(
647 &self,
648 requests: &[CredentialRequest],
649 subject_id: &str,
650 credential_data: &[Option<serde_json::Value>],
651 ) -> Result<Vec<CredentialResponse>> {
652 if requests.is_empty() {
653 return Err(AuthError::validation(
654 "Batch request must contain at least one credential request",
655 ));
656 }
657 if requests.len() != credential_data.len() {
658 return Err(AuthError::validation(
659 "Credential data array length must match requests array length",
660 ));
661 }
662
663 let mut responses = Vec::with_capacity(requests.len());
664 for (req, data) in requests.iter().zip(credential_data.iter()) {
665 let resp = self.issue_credential(req, subject_id, data.clone()).await?;
666 responses.push(resp);
667 }
668 Ok(responses)
669 }
670
671 pub async fn complete_deferred(
673 &self,
674 transaction_id: &str,
675 credential_data: serde_json::Value,
676 ) -> Result<CredentialResponse> {
677 let pending = self
678 .deferred
679 .write()
680 .await
681 .remove(transaction_id)
682 .ok_or_else(|| AuthError::validation("Unknown or expired transaction ID"))?;
683
684 let mut credential = match credential_data {
685 serde_json::Value::Object(map) => map,
686 _ => {
687 return Err(AuthError::validation(
688 "Deferred credential data must be a JSON object",
689 ));
690 }
691 };
692
693 credential
694 .entry("sub".to_string())
695 .or_insert_with(|| serde_json::Value::String(pending.subject_id.clone()));
696 credential.entry("format".to_string()).or_insert_with(|| {
697 serde_json::to_value(&pending.request.format).unwrap_or(serde_json::Value::Null)
698 });
699 credential
700 .entry("issuance_requested_at".to_string())
701 .or_insert_with(|| {
702 serde_json::Value::Number(serde_json::Number::from(pending.created_at))
703 });
704 if let Some(definition) = pending.request.credential_definition {
705 credential
706 .entry("credential_definition".to_string())
707 .or_insert_with(|| {
708 serde_json::to_value(definition).unwrap_or(serde_json::Value::Null)
709 });
710 }
711
712 Ok(CredentialResponse::completed(serde_json::Value::Object(
713 credential,
714 )))
715 }
716
717 pub async fn pending_count(&self) -> usize {
719 self.deferred.read().await.len()
720 }
721
722 pub async fn cleanup_nonces(&self) -> usize {
724 let now = SystemTime::now()
725 .duration_since(UNIX_EPOCH)
726 .unwrap_or_default()
727 .as_secs();
728 let mut nonces = self.nonces.write().await;
729 let before = nonces.len();
730 nonces.retain(|_, exp| *exp > now);
731 before - nonces.len()
732 }
733}
734
735#[cfg(test)]
736mod tests {
737 use super::*;
738
739 fn test_metadata() -> IssuerMetadata {
740 IssuerMetadata::builder("https://issuer.example.com")
741 .add_credential(
742 "UniversityDegree",
743 CredentialConfiguration::new(CredentialFormat::JwtVcJson)
744 .scope("degree")
745 .binding_methods(vec!["did:key"])
746 .signing_algorithms(vec!["ES256"])
747 .with_display(CredentialDisplay {
748 name: "University Degree".to_string(),
749 locale: Some("en".to_string()),
750 description: Some("A university degree credential".to_string()),
751 background_color: Some("#12107c".to_string()),
752 text_color: Some("#ffffff".to_string()),
753 })
754 .with_definition(CredentialDefinition {
755 types: vec![
756 "VerifiableCredential".to_string(),
757 "UniversityDegreeCredential".to_string(),
758 ],
759 credential_subject: None,
760 }),
761 )
762 .display("Example University", Some("en"))
763 .build()
764 }
765
766 #[test]
769 fn test_credential_format_serialization() {
770 assert_eq!(
771 serde_json::to_string(&CredentialFormat::JwtVcJson).unwrap(),
772 r#""jwt_vc_json""#
773 );
774 assert_eq!(
775 serde_json::to_string(&CredentialFormat::LdpVc).unwrap(),
776 r#""ldp_vc""#
777 );
778 assert_eq!(
779 serde_json::to_string(&CredentialFormat::SdJwtVc).unwrap(),
780 r#""vc+sd-jwt""#
781 );
782 }
783
784 #[test]
787 fn test_issuer_metadata_serialization() {
788 let meta = test_metadata();
789 let json = serde_json::to_value(&meta).unwrap();
790 assert_eq!(json["credential_issuer"], "https://issuer.example.com");
791 assert!(json["credential_configurations_supported"]["UniversityDegree"].is_object());
792 }
793
794 #[test]
795 fn test_metadata_roundtrip() {
796 let meta = test_metadata();
797 let json_str = serde_json::to_string(&meta).unwrap();
798 let parsed: IssuerMetadata = serde_json::from_str(&json_str).unwrap();
799 assert_eq!(parsed.credential_issuer, meta.credential_issuer);
800 assert!(
801 parsed
802 .credential_configurations_supported
803 .contains_key("UniversityDegree")
804 );
805 }
806
807 #[tokio::test]
810 async fn test_create_offer() {
811 let issuer = CredentialIssuer::new(test_metadata());
812 let offer = issuer
813 .create_offer(vec!["UniversityDegree".to_string()], "user-1")
814 .await
815 .unwrap();
816
817 assert_eq!(offer.credential_issuer, "https://issuer.example.com");
818 assert_eq!(offer.credential_configuration_ids, vec!["UniversityDegree"]);
819 let grants = offer.grants.unwrap();
820 assert!(grants.pre_authorized_code.is_some());
821 }
822
823 #[tokio::test]
824 async fn test_create_offer_invalid_config() {
825 let issuer = CredentialIssuer::new(test_metadata());
826 let result = issuer
827 .create_offer(vec!["NonExistent".to_string()], "user-1")
828 .await;
829 assert!(result.is_err());
830 }
831
832 #[tokio::test]
833 async fn test_create_offer_empty_configs() {
834 let issuer = CredentialIssuer::new(test_metadata());
835 let result = issuer.create_offer(vec![], "user-1").await;
836 assert!(result.is_err());
837 }
838
839 #[tokio::test]
842 async fn test_pre_auth_code_flow() {
843 let issuer = CredentialIssuer::new(test_metadata());
844 let offer = issuer
845 .create_offer(vec!["UniversityDegree".to_string()], "user-1")
846 .await
847 .unwrap();
848
849 let code = &offer
850 .grants
851 .unwrap()
852 .pre_authorized_code
853 .unwrap()
854 .pre_authorized_code;
855
856 let (config_id, subject_id) = issuer.validate_pre_auth_code(code).await.unwrap();
857 assert_eq!(config_id, "UniversityDegree");
858 assert_eq!(subject_id, "user-1");
859
860 assert!(issuer.validate_pre_auth_code(code).await.is_err());
862 }
863
864 #[tokio::test]
867 async fn test_nonce_lifecycle() {
868 let issuer = CredentialIssuer::new(test_metadata());
869 let nonce = issuer.generate_nonce(300).await;
870
871 assert!(issuer.validate_nonce(&nonce).await);
872 assert!(issuer.consume_nonce(&nonce).await);
873 assert!(!issuer.validate_nonce(&nonce).await);
875 assert!(!issuer.consume_nonce(&nonce).await);
876 }
877
878 #[tokio::test]
879 async fn test_nonce_invalid() {
880 let issuer = CredentialIssuer::new(test_metadata());
881 assert!(!issuer.validate_nonce("nonexistent").await);
882 }
883
884 #[tokio::test]
887 async fn test_issue_credential_immediate() {
888 let issuer = CredentialIssuer::new(test_metadata());
889 let request = CredentialRequest {
890 format: CredentialFormat::JwtVcJson,
891 credential_definition: None,
892 proof: None,
893 };
894
895 let cred_data = serde_json::json!({
896 "@context": ["https://www.w3.org/2018/credentials/v1"],
897 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
898 "credentialSubject": {
899 "degree": {
900 "type": "BachelorDegree",
901 "name": "Bachelor of Science"
902 }
903 }
904 });
905
906 let resp = issuer
907 .issue_credential(&request, "user-1", Some(cred_data))
908 .await
909 .unwrap();
910
911 assert!(resp.credential.is_some());
912 assert!(resp.transaction_id.is_none());
913 assert!(resp.c_nonce.is_some());
914 }
915
916 #[tokio::test]
917 async fn test_issue_credential_deferred() {
918 let issuer = CredentialIssuer::new(test_metadata());
919 let request = CredentialRequest {
920 format: CredentialFormat::JwtVcJson,
921 credential_definition: None,
922 proof: None,
923 };
924
925 let resp = issuer
926 .issue_credential(&request, "user-1", None)
927 .await
928 .unwrap();
929
930 assert!(resp.credential.is_none());
931 assert!(resp.transaction_id.is_some());
932 assert_eq!(issuer.pending_count().await, 1);
933
934 let tx_id = resp.transaction_id.unwrap();
936 let final_resp = issuer
937 .complete_deferred(&tx_id, serde_json::json!({"credential": "data"}))
938 .await
939 .unwrap();
940 assert!(final_resp.credential.is_some());
941 let completed = final_resp.credential.unwrap();
942 assert_eq!(completed["credential"], "data");
943 assert_eq!(completed["sub"], "user-1");
944 assert_eq!(completed["format"], "jwt_vc_json");
945 assert!(completed["issuance_requested_at"].is_number());
946 assert_eq!(issuer.pending_count().await, 0);
947 }
948
949 #[tokio::test]
950 async fn test_issue_credential_unsupported_format() {
951 let issuer = CredentialIssuer::new(test_metadata());
952 let request = CredentialRequest {
953 format: CredentialFormat::LdpVc,
954 credential_definition: None,
955 proof: None,
956 };
957
958 let result = issuer.issue_credential(&request, "user-1", None).await;
959 assert!(result.is_err());
960 }
961
962 #[tokio::test]
963 async fn test_issue_credential_invalid_proof_type() {
964 let issuer = CredentialIssuer::new(test_metadata());
965 let request = CredentialRequest {
966 format: CredentialFormat::JwtVcJson,
967 credential_definition: None,
968 proof: Some(CredentialProof {
969 proof_type: "ldp".to_string(),
970 jwt: None,
971 }),
972 };
973
974 let result = issuer
975 .issue_credential(&request, "user-1", Some(serde_json::json!({})))
976 .await;
977 assert!(result.is_err());
978 }
979
980 #[tokio::test]
981 async fn test_issue_credential_missing_jwt_proof() {
982 let issuer = CredentialIssuer::new(test_metadata());
983 let request = CredentialRequest {
984 format: CredentialFormat::JwtVcJson,
985 credential_definition: None,
986 proof: Some(CredentialProof {
987 proof_type: "jwt".to_string(),
988 jwt: None,
989 }),
990 };
991
992 let result = issuer
993 .issue_credential(&request, "user-1", Some(serde_json::json!({})))
994 .await;
995 assert!(result.is_err());
996 }
997
998 #[tokio::test]
1001 async fn test_complete_deferred_invalid_tx() {
1002 let issuer = CredentialIssuer::new(test_metadata());
1003 let result = issuer
1004 .complete_deferred("nonexistent", serde_json::json!({}))
1005 .await;
1006 assert!(result.is_err());
1007 }
1008
1009 #[test]
1012 fn test_credential_offer_roundtrip() {
1013 let offer = CredentialOffer {
1014 credential_issuer: "https://issuer.example".to_string(),
1015 credential_configuration_ids: vec!["DegreeCredential".to_string()],
1016 grants: Some(CredentialOfferGrants {
1017 authorization_code: None,
1018 pre_authorized_code: Some(PreAuthorizedCodeGrant {
1019 pre_authorized_code: "code123".to_string(),
1020 user_pin_required: true,
1021 }),
1022 }),
1023 };
1024 let json = serde_json::to_string(&offer).unwrap();
1025 let parsed: CredentialOffer = serde_json::from_str(&json).unwrap();
1026 assert_eq!(parsed.credential_issuer, offer.credential_issuer);
1027 assert!(
1028 parsed
1029 .grants
1030 .unwrap()
1031 .pre_authorized_code
1032 .unwrap()
1033 .user_pin_required
1034 );
1035 }
1036
1037 #[test]
1040 fn test_credential_request_serialization() {
1041 let req = CredentialRequest {
1042 format: CredentialFormat::JwtVcJson,
1043 credential_definition: Some(CredentialDefinition {
1044 types: vec!["VerifiableCredential".to_string()],
1045 credential_subject: None,
1046 }),
1047 proof: Some(CredentialProof {
1048 proof_type: "jwt".to_string(),
1049 jwt: Some("eyJ...".to_string()),
1050 }),
1051 };
1052 let json = serde_json::to_value(&req).unwrap();
1053 assert_eq!(json["format"], "jwt_vc_json");
1054 assert!(json["proof"]["jwt"].is_string());
1055 }
1056
1057 #[test]
1060 fn test_credential_response_immediate() {
1061 let resp = CredentialResponse::immediate(serde_json::json!({"vc": "data"}), "nonce-1", 300);
1062 assert!(resp.credential.is_some());
1063 assert!(resp.transaction_id.is_none());
1064 assert_eq!(resp.c_nonce.as_deref(), Some("nonce-1"));
1065 assert_eq!(resp.c_nonce_expires_in, Some(300));
1066 }
1067
1068 #[test]
1069 fn test_credential_response_deferred() {
1070 let resp = CredentialResponse::deferred("tx-1", "nonce-2", 600);
1071 assert!(resp.credential.is_none());
1072 assert_eq!(resp.transaction_id.as_deref(), Some("tx-1"));
1073 assert_eq!(resp.c_nonce.as_deref(), Some("nonce-2"));
1074 }
1075
1076 #[test]
1077 fn test_credential_response_completed() {
1078 let resp = CredentialResponse::completed(serde_json::json!({"done": true}));
1079 assert!(resp.credential.is_some());
1080 assert!(resp.transaction_id.is_none());
1081 assert!(resp.c_nonce.is_none());
1082 }
1083
1084 #[test]
1085 fn test_credential_configuration_builder() {
1086 let cfg = CredentialConfiguration::new(CredentialFormat::JwtVcJson)
1087 .scope("degree")
1088 .binding_methods(vec!["did:key"])
1089 .signing_algorithms(vec!["ES256", "EdDSA"]);
1090 assert_eq!(cfg.format, CredentialFormat::JwtVcJson);
1091 assert_eq!(cfg.scope.as_deref(), Some("degree"));
1092 assert_eq!(cfg.cryptographic_binding_methods_supported, vec!["did:key"]);
1093 assert_eq!(cfg.credential_signing_alg_values_supported.len(), 2);
1094 }
1095
1096 #[test]
1097 fn test_issuer_metadata_builder() {
1098 let meta = IssuerMetadata::builder("https://issuer.example.com")
1099 .add_credential(
1100 "TestCred",
1101 CredentialConfiguration::new(CredentialFormat::SdJwtVc).scope("test"),
1102 )
1103 .display("Test Issuer", None)
1104 .build();
1105 assert_eq!(meta.credential_issuer, "https://issuer.example.com");
1106 assert_eq!(
1107 meta.credential_endpoint,
1108 "https://issuer.example.com/credential"
1109 );
1110 assert!(
1111 meta.credential_configurations_supported
1112 .contains_key("TestCred")
1113 );
1114 assert_eq!(meta.display[0].name, "Test Issuer");
1115 assert!(meta.display[0].locale.is_none());
1116 }
1117
1118 #[test]
1119 fn test_issuer_metadata_builder_custom_endpoint() {
1120 let meta = IssuerMetadata::builder("https://example.com")
1121 .credential_endpoint("https://example.com/api/vc/issue")
1122 .batch_credential_endpoint("https://example.com/api/vc/batch")
1123 .build();
1124 assert_eq!(meta.credential_endpoint, "https://example.com/api/vc/issue");
1125 assert_eq!(
1126 meta.batch_credential_endpoint.as_deref(),
1127 Some("https://example.com/api/vc/batch")
1128 );
1129 }
1130}