1use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::fmt;
23use uuid::Uuid;
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct Did(pub String);
37
38impl Did {
39 pub fn web(domain: &str, agent_id: Uuid) -> Self {
54 Self(format!("did:web:{}:agents:{}", domain, agent_id))
55 }
56
57 pub fn parse(s: &str) -> Option<Self> {
59 if s.starts_with("did:") {
60 Some(Self(s.to_string()))
61 } else {
62 None
63 }
64 }
65
66 pub fn agent_id(&self) -> Option<Uuid> {
71 let parts: Vec<&str> = self.0.split(':').collect();
72 if parts.len() < 5 || parts[1] != "web" {
74 return None;
75 }
76 let mut iter = parts.iter();
78 while let Some(&segment) = iter.next() {
79 if segment == "agents" {
80 if let Some(&uuid_str) = iter.next() {
81 return Uuid::parse_str(uuid_str).ok();
82 }
83 }
84 }
85 None
86 }
87
88 pub fn method(&self) -> &str {
92 self.0
93 .strip_prefix("did:")
94 .and_then(|rest| rest.split(':').next())
95 .unwrap_or("")
96 }
97}
98
99impl fmt::Display for Did {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 write!(f, "{}", self.0)
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct DidDocument {
115 #[serde(rename = "@context")]
117 pub context: Vec<String>,
118 pub id: Did,
120 pub controller: Did,
122 pub verification_method: Vec<VerificationMethod>,
124 pub authentication: Vec<String>,
126 pub assertion_method: Vec<String>,
128 pub service: Vec<ServiceEndpoint>,
130 pub created: DateTime<Utc>,
132 pub updated: DateTime<Utc>,
134}
135
136impl DidDocument {
137 pub fn for_agent(domain: &str, agent_id: Uuid, tenant_id: Uuid) -> Self {
142 let agent_did = Did::web(domain, agent_id);
143 let tenant_did = Did::web(domain, tenant_id);
144 let now = Utc::now();
145
146 let key_id = format!("{}#key-1", agent_did.0);
147
148 Self {
149 context: vec![
150 "https://www.w3.org/ns/did/v1".to_string(),
151 "https://w3id.org/security/suites/ed25519-2020/v1".to_string(),
152 ],
153 id: agent_did.clone(),
154 controller: tenant_did,
155 verification_method: vec![VerificationMethod {
156 id: key_id.clone(),
157 method_type: "Ed25519VerificationKey2020".to_string(),
158 controller: agent_did.0.clone(),
159 public_key_multibase: None, }],
161 authentication: vec![key_id.clone()],
162 assertion_method: vec![key_id],
163 service: vec![ServiceEndpoint {
164 id: format!("{}#cellstate-api", agent_did.0),
165 service_type: "CellstateAgent".to_string(),
166 service_endpoint: format!("https://{}/api/v1/agents/{}", domain, agent_id),
167 }],
168 created: now,
169 updated: now,
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct VerificationMethod {
177 pub id: String,
179 #[serde(rename = "type")]
181 pub method_type: String,
182 pub controller: String,
184 pub public_key_multibase: Option<String>,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ServiceEndpoint {
191 pub id: String,
193 #[serde(rename = "type")]
195 pub service_type: String,
196 #[serde(rename = "serviceEndpoint")]
198 pub service_endpoint: String,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct VerifiableCredential {
212 #[serde(rename = "@context")]
214 pub context: Vec<String>,
215 pub id: String,
217 #[serde(rename = "type")]
219 pub credential_type: Vec<String>,
220 pub issuer: CredentialIssuer,
222 pub issuance_date: DateTime<Utc>,
224 pub expiration_date: Option<DateTime<Utc>>,
226 pub credential_subject: CredentialSubject,
228 pub proof: Option<CredentialProof>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(untagged)]
235pub enum CredentialIssuer {
236 Simple(String),
238 Object {
240 id: String,
242 name: Option<String>,
244 },
245}
246
247impl CredentialIssuer {
248 pub fn id(&self) -> &str {
250 match self {
251 CredentialIssuer::Simple(id) => id,
252 CredentialIssuer::Object { id, .. } => id,
253 }
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct CredentialSubject {
260 pub id: String,
262 #[serde(flatten)]
264 pub claims: HashMap<String, serde_json::Value>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct CredentialProof {
270 #[serde(rename = "type")]
272 pub proof_type: String,
273 pub created: DateTime<Utc>,
275 pub verification_method: String,
277 pub proof_purpose: String,
279 pub proof_value: String,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum AgentCredentialType {
291 Identity,
293 Capability,
295 Provenance,
297 Compliance,
299}
300
301impl AgentCredentialType {
302 pub fn vc_type(&self) -> &'static str {
304 match self {
305 AgentCredentialType::Identity => "AgentIdentityCredential",
306 AgentCredentialType::Capability => "AgentCapabilityCredential",
307 AgentCredentialType::Provenance => "AgentProvenanceCredential",
308 AgentCredentialType::Compliance => "AgentComplianceCredential",
309 }
310 }
311}
312
313impl fmt::Display for AgentCredentialType {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 write!(f, "{}", self.vc_type())
316 }
317}
318
319pub struct CredentialBuilder {
341 issuer: Option<String>,
342 subject: Option<String>,
343 credential_type: Option<AgentCredentialType>,
344 claims: HashMap<String, serde_json::Value>,
345 expiration: Option<DateTime<Utc>>,
346}
347
348impl CredentialBuilder {
349 pub fn new() -> Self {
351 Self {
352 issuer: None,
353 subject: None,
354 credential_type: None,
355 claims: HashMap::new(),
356 expiration: None,
357 }
358 }
359
360 pub fn issuer(mut self, issuer: &str) -> Self {
362 self.issuer = Some(issuer.to_string());
363 self
364 }
365
366 pub fn subject(mut self, subject_did: &str) -> Self {
368 self.subject = Some(subject_did.to_string());
369 self
370 }
371
372 pub fn credential_type(mut self, cred_type: AgentCredentialType) -> Self {
374 self.credential_type = Some(cred_type);
375 self
376 }
377
378 pub fn claim(mut self, key: &str, value: serde_json::Value) -> Self {
380 self.claims.insert(key.to_string(), value);
381 self
382 }
383
384 pub fn expiration(mut self, expires: DateTime<Utc>) -> Self {
386 self.expiration = Some(expires);
387 self
388 }
389
390 pub fn build(self) -> Result<VerifiableCredential, CredentialError> {
394 let issuer = self.issuer.ok_or(CredentialError::MissingIssuer)?;
395 let subject = self.subject.ok_or(CredentialError::MissingSubject)?;
396
397 let mut types = vec!["VerifiableCredential".to_string()];
398 if let Some(ref cred_type) = self.credential_type {
399 types.push(cred_type.vc_type().to_string());
400 }
401
402 let credential_id = format!("urn:uuid:{}", Uuid::now_v7());
403
404 Ok(VerifiableCredential {
405 context: vec![
406 "https://www.w3.org/2018/credentials/v1".to_string(),
407 "https://cellstate.batterypack.dev/credentials/v1".to_string(),
408 ],
409 id: credential_id,
410 credential_type: types,
411 issuer: CredentialIssuer::Simple(issuer),
412 issuance_date: Utc::now(),
413 expiration_date: self.expiration,
414 credential_subject: CredentialSubject {
415 id: subject,
416 claims: self.claims,
417 },
418 proof: None,
419 })
420 }
421}
422
423impl Default for CredentialBuilder {
424 fn default() -> Self {
425 Self::new()
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
435pub enum CredentialError {
436 MissingIssuer,
438 MissingSubject,
440 InvalidDid(String),
442 Expired,
444 InvalidProof,
446 UnsupportedProofType(String),
448}
449
450impl fmt::Display for CredentialError {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 match self {
453 CredentialError::MissingIssuer => write!(f, "Credential is missing an issuer"),
454 CredentialError::MissingSubject => write!(f, "Credential is missing a subject"),
455 CredentialError::InvalidDid(did) => write!(f, "Invalid DID: {}", did),
456 CredentialError::Expired => write!(f, "Credential has expired"),
457 CredentialError::InvalidProof => write!(f, "Credential proof is invalid"),
458 CredentialError::UnsupportedProofType(t) => {
459 write!(f, "Unsupported proof type: {}", t)
460 }
461 }
462 }
463}
464
465impl std::error::Error for CredentialError {}
466
467pub struct CredentialVerifier;
477
478impl CredentialVerifier {
479 pub fn verify_structure(credential: &VerifiableCredential) -> Result<(), CredentialError> {
490 if credential.id.is_empty() {
492 return Err(CredentialError::InvalidDid(
493 "Credential ID is empty".to_string(),
494 ));
495 }
496
497 if !credential
499 .credential_type
500 .iter()
501 .any(|t| t == "VerifiableCredential")
502 {
503 return Err(CredentialError::InvalidDid(
504 "Missing VerifiableCredential base type".to_string(),
505 ));
506 }
507
508 if credential.credential_subject.id.is_empty() {
510 return Err(CredentialError::MissingSubject);
511 }
512
513 if Self::is_expired(credential) {
515 return Err(CredentialError::Expired);
516 }
517
518 Ok(())
519 }
520
521 pub fn is_expired(credential: &VerifiableCredential) -> bool {
525 if let Some(expiration) = credential.expiration_date {
526 Utc::now() > expiration
527 } else {
528 false
529 }
530 }
531
532 pub fn verify_subject(credential: &VerifiableCredential, subject_did: &str) -> bool {
534 credential.credential_subject.id == subject_did
535 }
536}
537
538#[cfg(test)]
543mod tests {
544 use super::*;
545 use serde_json::json;
546
547 #[test]
552 fn test_did_web_creation() {
553 let agent_id = Uuid::nil();
554 let did = Did::web("cellstate.batterypack.dev", agent_id);
555 assert_eq!(
556 did.0,
557 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000"
558 );
559 }
560
561 #[test]
562 fn test_did_web_with_real_uuid() {
563 let agent_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
564 let did = Did::web("example.com", agent_id);
565 assert_eq!(
566 did.0,
567 "did:web:example.com:agents:550e8400-e29b-41d4-a716-446655440000"
568 );
569 }
570
571 #[test]
572 fn test_did_parse_valid() {
573 let did = Did::parse(
574 "did:web:cellstate.batterypack.dev:agents:550e8400-e29b-41d4-a716-446655440000",
575 );
576 assert!(did.is_some());
577 assert_eq!(
578 did.unwrap().0,
579 "did:web:cellstate.batterypack.dev:agents:550e8400-e29b-41d4-a716-446655440000"
580 );
581 }
582
583 #[test]
584 fn test_did_parse_other_method() {
585 let did = Did::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
586 assert!(did.is_some());
587 }
588
589 #[test]
590 fn test_did_parse_invalid() {
591 assert!(Did::parse("not-a-did").is_none());
592 assert!(Did::parse("").is_none());
593 assert!(Did::parse("http://example.com").is_none());
594 }
595
596 #[test]
597 fn test_did_agent_id_extraction() {
598 let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
599 let did = Did::web("cellstate.batterypack.dev", uuid);
600 assert_eq!(did.agent_id(), Some(uuid));
601 }
602
603 #[test]
604 fn test_did_agent_id_nil() {
605 let did = Did::web("cellstate.batterypack.dev", Uuid::nil());
606 assert_eq!(did.agent_id(), Some(Uuid::nil()));
607 }
608
609 #[test]
610 fn test_did_agent_id_non_web() {
611 let did = Did("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string());
612 assert!(did.agent_id().is_none());
613 }
614
615 #[test]
616 fn test_did_agent_id_malformed() {
617 let did = Did("did:web:cellstate.batterypack.dev".to_string());
618 assert!(did.agent_id().is_none());
619 }
620
621 #[test]
622 fn test_did_agent_id_bad_uuid() {
623 let did = Did("did:web:cellstate.batterypack.dev:agents:not-a-uuid".to_string());
624 assert!(did.agent_id().is_none());
625 }
626
627 #[test]
628 fn test_did_method_web() {
629 let did = Did::web("cellstate.batterypack.dev", Uuid::nil());
630 assert_eq!(did.method(), "web");
631 }
632
633 #[test]
634 fn test_did_method_key() {
635 let did = Did("did:key:z6Mk...".to_string());
636 assert_eq!(did.method(), "key");
637 }
638
639 #[test]
640 fn test_did_method_empty() {
641 let did = Did("not-a-did".to_string());
642 assert_eq!(did.method(), "");
643 }
644
645 #[test]
646 fn test_did_display() {
647 let did = Did::web("cellstate.batterypack.dev", Uuid::nil());
648 let displayed = format!("{}", did);
649 assert_eq!(
650 displayed,
651 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000"
652 );
653 }
654
655 #[test]
656 fn test_did_serde_roundtrip() {
657 let did = Did::web("cellstate.batterypack.dev", Uuid::nil());
658 let json = serde_json::to_string(&did).unwrap();
659 let deserialized: Did = serde_json::from_str(&json).unwrap();
660 assert_eq!(did, deserialized);
661 }
662
663 #[test]
664 fn test_did_equality_and_hash() {
665 let id = Uuid::nil();
666 let did1 = Did::web("cellstate.batterypack.dev", id);
667 let did2 = Did::web("cellstate.batterypack.dev", id);
668 let did3 = Did::web("other.com", id);
669
670 assert_eq!(did1, did2);
671 assert_ne!(did1, did3);
672
673 let mut map = HashMap::new();
675 map.insert(did1.clone(), "value");
676 assert_eq!(map.get(&did2), Some(&"value"));
677 assert_eq!(map.get(&did3), None);
678 }
679
680 #[test]
685 fn test_did_document_for_agent() {
686 let agent_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
687 let tenant_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440000").unwrap();
688 let doc = DidDocument::for_agent("cellstate.batterypack.dev", agent_id, tenant_id);
689
690 assert_eq!(doc.context.len(), 2);
692 assert!(doc.context[0].contains("did/v1"));
693
694 assert_eq!(doc.id, Did::web("cellstate.batterypack.dev", agent_id));
696
697 assert_eq!(
699 doc.controller,
700 Did::web("cellstate.batterypack.dev", tenant_id)
701 );
702
703 assert_eq!(doc.verification_method.len(), 1);
705 assert_eq!(
706 doc.verification_method[0].method_type,
707 "Ed25519VerificationKey2020"
708 );
709 assert!(doc.verification_method[0].id.contains("#key-1"));
710
711 assert_eq!(doc.authentication.len(), 1);
713 assert!(doc.authentication[0].contains("#key-1"));
714 assert_eq!(doc.assertion_method.len(), 1);
715
716 assert_eq!(doc.service.len(), 1);
718 assert_eq!(doc.service[0].service_type, "CellstateAgent");
719 assert!(doc.service[0]
720 .service_endpoint
721 .contains(&agent_id.to_string()));
722 }
723
724 #[test]
725 fn test_did_document_serde_roundtrip() {
726 let doc = DidDocument::for_agent("cellstate.batterypack.dev", Uuid::nil(), Uuid::nil());
727 let json = serde_json::to_string(&doc).unwrap();
728 let deserialized: DidDocument = serde_json::from_str(&json).unwrap();
729 assert_eq!(deserialized.id, doc.id);
730 assert_eq!(deserialized.controller, doc.controller);
731 assert_eq!(deserialized.verification_method.len(), 1);
732 assert_eq!(deserialized.service.len(), 1);
733 }
734
735 #[test]
736 fn test_did_document_json_ld_context() {
737 let doc = DidDocument::for_agent("cellstate.batterypack.dev", Uuid::nil(), Uuid::nil());
738 let json_value: serde_json::Value = serde_json::to_value(&doc).unwrap();
739 assert!(json_value.get("@context").is_some());
741 let ctx = json_value["@context"].as_array().unwrap();
742 assert_eq!(ctx.len(), 2);
743 }
744
745 #[test]
750 fn test_credential_builder_identity() {
751 let cred = CredentialBuilder::new()
752 .issuer("did:web:cellstate.batterypack.dev")
753 .subject(
754 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
755 )
756 .credential_type(AgentCredentialType::Identity)
757 .claim("name", json!("test-agent"))
758 .claim("agentType", json!("coder"))
759 .build()
760 .unwrap();
761
762 assert!(cred.id.starts_with("urn:uuid:"));
763 assert!(cred
764 .credential_type
765 .contains(&"VerifiableCredential".to_string()));
766 assert!(cred
767 .credential_type
768 .contains(&"AgentIdentityCredential".to_string()));
769 assert_eq!(cred.credential_subject.claims["name"], json!("test-agent"));
770 assert_eq!(cred.credential_subject.claims["agentType"], json!("coder"));
771 assert!(cred.expiration_date.is_none());
772 assert!(cred.proof.is_none());
773 }
774
775 #[test]
776 fn test_credential_builder_capability() {
777 let cred = CredentialBuilder::new()
778 .issuer("did:web:cellstate.batterypack.dev")
779 .subject(
780 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
781 )
782 .credential_type(AgentCredentialType::Capability)
783 .claim("capabilities", json!(["read", "write", "execute"]))
784 .build()
785 .unwrap();
786
787 assert!(cred
788 .credential_type
789 .contains(&"AgentCapabilityCredential".to_string()));
790 assert_eq!(
791 cred.credential_subject.claims["capabilities"],
792 json!(["read", "write", "execute"])
793 );
794 }
795
796 #[test]
797 fn test_credential_builder_provenance() {
798 let cred = CredentialBuilder::new()
799 .issuer("did:web:cellstate.batterypack.dev")
800 .subject(
801 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
802 )
803 .credential_type(AgentCredentialType::Provenance)
804 .claim("creator", json!("tenant-admin"))
805 .claim("createdAt", json!("2025-01-01T00:00:00Z"))
806 .build()
807 .unwrap();
808
809 assert!(cred
810 .credential_type
811 .contains(&"AgentProvenanceCredential".to_string()));
812 }
813
814 #[test]
815 fn test_credential_builder_compliance() {
816 let cred = CredentialBuilder::new()
817 .issuer("did:web:cellstate.batterypack.dev")
818 .subject(
819 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
820 )
821 .credential_type(AgentCredentialType::Compliance)
822 .claim("regulation", json!("EU AI Act"))
823 .claim("riskCategory", json!("limited"))
824 .claim("transparencyProvided", json!(true))
825 .build()
826 .unwrap();
827
828 assert!(cred
829 .credential_type
830 .contains(&"AgentComplianceCredential".to_string()));
831 assert_eq!(
832 cred.credential_subject.claims["regulation"],
833 json!("EU AI Act")
834 );
835 }
836
837 #[test]
838 fn test_credential_builder_with_expiration() {
839 let expires = Utc::now() + chrono::Duration::days(365);
840 let cred = CredentialBuilder::new()
841 .issuer("did:web:cellstate.batterypack.dev")
842 .subject(
843 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
844 )
845 .credential_type(AgentCredentialType::Identity)
846 .expiration(expires)
847 .build()
848 .unwrap();
849
850 assert_eq!(cred.expiration_date, Some(expires));
851 }
852
853 #[test]
854 fn test_credential_builder_no_type() {
855 let cred = CredentialBuilder::new()
856 .issuer("did:web:cellstate.batterypack.dev")
857 .subject(
858 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
859 )
860 .build()
861 .unwrap();
862
863 assert_eq!(cred.credential_type, vec!["VerifiableCredential"]);
865 }
866
867 #[test]
868 fn test_credential_builder_missing_issuer() {
869 let result = CredentialBuilder::new()
870 .subject(
871 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
872 )
873 .build();
874
875 assert_eq!(result.unwrap_err(), CredentialError::MissingIssuer);
876 }
877
878 #[test]
879 fn test_credential_builder_missing_subject() {
880 let result = CredentialBuilder::new()
881 .issuer("did:web:cellstate.batterypack.dev")
882 .build();
883
884 assert_eq!(result.unwrap_err(), CredentialError::MissingSubject);
885 }
886
887 #[test]
888 fn test_credential_builder_missing_both() {
889 let result = CredentialBuilder::new().build();
890 assert_eq!(result.unwrap_err(), CredentialError::MissingIssuer);
892 }
893
894 #[test]
895 fn test_credential_builder_default() {
896 let builder = CredentialBuilder::default();
897 assert!(builder.build().is_err());
899 }
900
901 fn make_valid_credential() -> VerifiableCredential {
906 CredentialBuilder::new()
907 .issuer("did:web:cellstate.batterypack.dev")
908 .subject(
909 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
910 )
911 .credential_type(AgentCredentialType::Identity)
912 .claim("name", json!("test-agent"))
913 .build()
914 .unwrap()
915 }
916
917 #[test]
918 fn test_verify_structure_valid() {
919 let cred = make_valid_credential();
920 assert!(CredentialVerifier::verify_structure(&cred).is_ok());
921 }
922
923 #[test]
924 fn test_verify_structure_empty_id() {
925 let mut cred = make_valid_credential();
926 cred.id = String::new();
927 let result = CredentialVerifier::verify_structure(&cred);
928 assert!(matches!(result, Err(CredentialError::InvalidDid(_))));
929 }
930
931 #[test]
932 fn test_verify_structure_missing_base_type() {
933 let mut cred = make_valid_credential();
934 cred.credential_type = vec!["AgentIdentityCredential".to_string()];
935 let result = CredentialVerifier::verify_structure(&cred);
936 assert!(matches!(result, Err(CredentialError::InvalidDid(_))));
937 }
938
939 #[test]
940 fn test_verify_structure_empty_subject() {
941 let mut cred = make_valid_credential();
942 cred.credential_subject.id = String::new();
943 let result = CredentialVerifier::verify_structure(&cred);
944 assert_eq!(result.unwrap_err(), CredentialError::MissingSubject);
945 }
946
947 #[test]
948 fn test_verify_structure_expired() {
949 let mut cred = make_valid_credential();
950 cred.expiration_date = Some(Utc::now() - chrono::Duration::hours(1));
951 let result = CredentialVerifier::verify_structure(&cred);
952 assert_eq!(result.unwrap_err(), CredentialError::Expired);
953 }
954
955 #[test]
956 fn test_is_expired_no_expiration() {
957 let cred = make_valid_credential();
958 assert!(!CredentialVerifier::is_expired(&cred));
959 }
960
961 #[test]
962 fn test_is_expired_future() {
963 let mut cred = make_valid_credential();
964 cred.expiration_date = Some(Utc::now() + chrono::Duration::days(365));
965 assert!(!CredentialVerifier::is_expired(&cred));
966 }
967
968 #[test]
969 fn test_is_expired_past() {
970 let mut cred = make_valid_credential();
971 cred.expiration_date = Some(Utc::now() - chrono::Duration::seconds(1));
972 assert!(CredentialVerifier::is_expired(&cred));
973 }
974
975 #[test]
976 fn test_verify_subject_match() {
977 let cred = make_valid_credential();
978 assert!(CredentialVerifier::verify_subject(
979 &cred,
980 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000"
981 ));
982 }
983
984 #[test]
985 fn test_verify_subject_mismatch() {
986 let cred = make_valid_credential();
987 assert!(!CredentialVerifier::verify_subject(
988 &cred,
989 "did:web:cellstate.batterypack.dev:agents:11111111-1111-1111-1111-111111111111"
990 ));
991 }
992
993 #[test]
994 fn test_verify_subject_empty() {
995 let cred = make_valid_credential();
996 assert!(!CredentialVerifier::verify_subject(&cred, ""));
997 }
998
999 #[test]
1004 fn test_credential_issuer_simple_id() {
1005 let issuer = CredentialIssuer::Simple("did:web:cellstate.batterypack.dev".to_string());
1006 assert_eq!(issuer.id(), "did:web:cellstate.batterypack.dev");
1007 }
1008
1009 #[test]
1010 fn test_credential_issuer_object_id() {
1011 let issuer = CredentialIssuer::Object {
1012 id: "did:web:cellstate.batterypack.dev".to_string(),
1013 name: Some("CELLSTATE Platform".to_string()),
1014 };
1015 assert_eq!(issuer.id(), "did:web:cellstate.batterypack.dev");
1016 }
1017
1018 #[test]
1019 fn test_credential_issuer_serde_simple() {
1020 let issuer = CredentialIssuer::Simple("did:web:cellstate.batterypack.dev".to_string());
1021 let json = serde_json::to_string(&issuer).unwrap();
1022 assert_eq!(json, "\"did:web:cellstate.batterypack.dev\"");
1023 }
1024
1025 #[test]
1026 fn test_credential_issuer_serde_object() {
1027 let issuer = CredentialIssuer::Object {
1028 id: "did:web:cellstate.batterypack.dev".to_string(),
1029 name: Some("CELLSTATE".to_string()),
1030 };
1031 let json = serde_json::to_value(&issuer).unwrap();
1032 assert_eq!(json["id"], "did:web:cellstate.batterypack.dev");
1033 assert_eq!(json["name"], "CELLSTATE");
1034 }
1035
1036 #[test]
1041 fn test_credential_serde_roundtrip() {
1042 let cred = CredentialBuilder::new()
1043 .issuer("did:web:cellstate.batterypack.dev")
1044 .subject(
1045 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
1046 )
1047 .credential_type(AgentCredentialType::Identity)
1048 .claim("name", json!("agent-1"))
1049 .claim("version", json!(1))
1050 .build()
1051 .unwrap();
1052
1053 let json = serde_json::to_string(&cred).unwrap();
1054 let deserialized: VerifiableCredential = serde_json::from_str(&json).unwrap();
1055
1056 assert_eq!(deserialized.id, cred.id);
1057 assert_eq!(deserialized.credential_type, cred.credential_type);
1058 assert_eq!(
1059 deserialized.credential_subject.id,
1060 cred.credential_subject.id
1061 );
1062 assert_eq!(
1063 deserialized.credential_subject.claims["name"],
1064 json!("agent-1")
1065 );
1066 }
1067
1068 #[test]
1069 fn test_credential_json_structure() {
1070 let cred = make_valid_credential();
1071 let json_value: serde_json::Value = serde_json::to_value(&cred).unwrap();
1072
1073 assert!(json_value.get("@context").is_some());
1075 assert!(json_value.get("type").is_some());
1077 let types = json_value["type"].as_array().unwrap();
1078 assert!(types.iter().any(|t| t == "VerifiableCredential"));
1079 }
1080
1081 #[test]
1086 fn test_credential_error_display() {
1087 assert_eq!(
1088 format!("{}", CredentialError::MissingIssuer),
1089 "Credential is missing an issuer"
1090 );
1091 assert_eq!(
1092 format!("{}", CredentialError::MissingSubject),
1093 "Credential is missing a subject"
1094 );
1095 assert_eq!(
1096 format!("{}", CredentialError::InvalidDid("bad".to_string())),
1097 "Invalid DID: bad"
1098 );
1099 assert_eq!(
1100 format!("{}", CredentialError::Expired),
1101 "Credential has expired"
1102 );
1103 assert_eq!(
1104 format!("{}", CredentialError::InvalidProof),
1105 "Credential proof is invalid"
1106 );
1107 assert_eq!(
1108 format!(
1109 "{}",
1110 CredentialError::UnsupportedProofType("RSA".to_string())
1111 ),
1112 "Unsupported proof type: RSA"
1113 );
1114 }
1115
1116 #[test]
1117 fn test_credential_error_is_error() {
1118 let err: Box<dyn std::error::Error> = Box::new(CredentialError::MissingIssuer);
1119 assert_eq!(err.to_string(), "Credential is missing an issuer");
1120 }
1121
1122 #[test]
1127 fn test_agent_credential_type_vc_type() {
1128 assert_eq!(
1129 AgentCredentialType::Identity.vc_type(),
1130 "AgentIdentityCredential"
1131 );
1132 assert_eq!(
1133 AgentCredentialType::Capability.vc_type(),
1134 "AgentCapabilityCredential"
1135 );
1136 assert_eq!(
1137 AgentCredentialType::Provenance.vc_type(),
1138 "AgentProvenanceCredential"
1139 );
1140 assert_eq!(
1141 AgentCredentialType::Compliance.vc_type(),
1142 "AgentComplianceCredential"
1143 );
1144 }
1145
1146 #[test]
1147 fn test_agent_credential_type_display() {
1148 assert_eq!(
1149 format!("{}", AgentCredentialType::Identity),
1150 "AgentIdentityCredential"
1151 );
1152 }
1153
1154 #[test]
1155 fn test_agent_credential_type_equality() {
1156 assert_eq!(AgentCredentialType::Identity, AgentCredentialType::Identity);
1157 assert_ne!(
1158 AgentCredentialType::Identity,
1159 AgentCredentialType::Capability
1160 );
1161 }
1162
1163 #[test]
1164 fn test_agent_credential_type_serde_roundtrip() {
1165 let cred_type = AgentCredentialType::Compliance;
1166 let json = serde_json::to_string(&cred_type).unwrap();
1167 let deserialized: AgentCredentialType = serde_json::from_str(&json).unwrap();
1168 assert_eq!(cred_type, deserialized);
1169 }
1170
1171 #[test]
1176 fn test_credential_proof_serde() {
1177 let proof = CredentialProof {
1178 proof_type: "Ed25519Signature2020".to_string(),
1179 created: Utc::now(),
1180 verification_method: "did:web:cellstate.batterypack.dev#key-1".to_string(),
1181 proof_purpose: "assertionMethod".to_string(),
1182 proof_value: "z58DAdFfa9SkqZMVPxAQpic7ndTn".to_string(),
1183 };
1184
1185 let json = serde_json::to_string(&proof).unwrap();
1186 let deserialized: CredentialProof = serde_json::from_str(&json).unwrap();
1187 assert_eq!(deserialized.proof_type, "Ed25519Signature2020");
1188 assert_eq!(
1189 deserialized.verification_method,
1190 "did:web:cellstate.batterypack.dev#key-1"
1191 );
1192 assert_eq!(deserialized.proof_purpose, "assertionMethod");
1193 }
1194
1195 #[test]
1200 fn test_full_credential_lifecycle() {
1201 let agent_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1202 let tenant_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440000").unwrap();
1203 let domain = "cellstate.batterypack.dev";
1204
1205 let doc = DidDocument::for_agent(domain, agent_id, tenant_id);
1207 assert_eq!(doc.id, Did::web(domain, agent_id));
1208
1209 let cred = CredentialBuilder::new()
1211 .issuer(&doc.controller.0)
1212 .subject(&doc.id.0)
1213 .credential_type(AgentCredentialType::Identity)
1214 .claim("name", json!("agent-alpha"))
1215 .claim("agentType", json!("coder"))
1216 .claim("tenantId", json!(tenant_id.to_string()))
1217 .build()
1218 .unwrap();
1219
1220 assert!(CredentialVerifier::verify_structure(&cred).is_ok());
1222
1223 assert!(CredentialVerifier::verify_subject(&cred, &doc.id.0));
1225
1226 assert!(!CredentialVerifier::is_expired(&cred));
1228
1229 assert_eq!(doc.id.agent_id(), Some(agent_id));
1231 }
1232
1233 #[test]
1234 fn test_multiple_credential_types_for_agent() {
1235 let agent_did =
1236 "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000";
1237 let issuer = "did:web:cellstate.batterypack.dev";
1238
1239 let identity = CredentialBuilder::new()
1240 .issuer(issuer)
1241 .subject(agent_did)
1242 .credential_type(AgentCredentialType::Identity)
1243 .claim("name", json!("agent-1"))
1244 .build()
1245 .unwrap();
1246
1247 let capability = CredentialBuilder::new()
1248 .issuer(issuer)
1249 .subject(agent_did)
1250 .credential_type(AgentCredentialType::Capability)
1251 .claim("capabilities", json!(["read", "write"]))
1252 .build()
1253 .unwrap();
1254
1255 let provenance = CredentialBuilder::new()
1256 .issuer(issuer)
1257 .subject(agent_did)
1258 .credential_type(AgentCredentialType::Provenance)
1259 .claim("creator", json!("admin"))
1260 .build()
1261 .unwrap();
1262
1263 let compliance = CredentialBuilder::new()
1264 .issuer(issuer)
1265 .subject(agent_did)
1266 .credential_type(AgentCredentialType::Compliance)
1267 .claim("regulation", json!("EU AI Act"))
1268 .build()
1269 .unwrap();
1270
1271 for cred in [&identity, &capability, &provenance, &compliance] {
1273 assert!(CredentialVerifier::verify_structure(cred).is_ok());
1274 assert!(CredentialVerifier::verify_subject(cred, agent_did));
1275 }
1276
1277 let ids: Vec<&str> = [&identity, &capability, &provenance, &compliance]
1279 .iter()
1280 .map(|c| c.id.as_str())
1281 .collect();
1282 let unique: std::collections::HashSet<&&str> = ids.iter().collect();
1283 assert_eq!(unique.len(), 4);
1284 }
1285
1286 #[test]
1287 fn test_verification_method_serde() {
1288 let vm = VerificationMethod {
1289 id: "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000#key-1".to_string(),
1290 method_type: "Ed25519VerificationKey2020".to_string(),
1291 controller: "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000"
1292 .to_string(),
1293 public_key_multibase: Some("z6MkhaXgBZDvotDk".to_string()),
1294 };
1295
1296 let json = serde_json::to_value(&vm).unwrap();
1297 assert_eq!(json["type"], "Ed25519VerificationKey2020");
1298 assert!(
1299 json.get("publicKeyMultibase").is_some() || json.get("public_key_multibase").is_some()
1300 );
1301 }
1302
1303 #[test]
1304 fn test_service_endpoint_serde() {
1305 let ep = ServiceEndpoint {
1306 id: "did:web:cellstate.batterypack.dev#api".to_string(),
1307 service_type: "CellstateAgent".to_string(),
1308 service_endpoint: "https://cellstate.batterypack.dev/api/v1/agents/test".to_string(),
1309 };
1310
1311 let json = serde_json::to_value(&ep).unwrap();
1312 assert_eq!(json["type"], "CellstateAgent");
1313 assert_eq!(
1314 json["serviceEndpoint"],
1315 "https://cellstate.batterypack.dev/api/v1/agents/test"
1316 );
1317 }
1318}