use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use trust_tasks_rs::{
handlers::{InMemoryHandler, NoopHandler},
ErrorPayload, JsonLdContext, StandardCode, TransportHandler, TrustTask, TrustTaskCode, TypeUri,
};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct KycHandoff {
subject: String,
result: String,
level: String,
}
#[test]
fn spec_example_1_round_trips() {
let json = r#"{
"id": "4f3c9e2a-1b81-4d3e-9b51-7a3c89e3d1f2",
"type": "https://trusttasks.org/spec/kyc-handoff/1.0",
"issuer": "did:web:verifier.example",
"recipient": "did:web:bank.example",
"issuedAt": "2026-04-12T09:31:00Z",
"expiresAt": "2027-04-12T09:31:00Z",
"payload": {
"subject": "did:key:z6Mk...",
"result": "passed",
"level": "LOA2"
}
}"#;
let doc: TrustTask<KycHandoff> = serde_json::from_str(json).unwrap();
assert_eq!(
doc.type_uri,
"https://trusttasks.org/spec/kyc-handoff/1.0"
.parse()
.unwrap()
);
let rendered: serde_json::Value = serde_json::to_value(&doc).unwrap();
let expected: serde_json::Value = serde_json::from_str(json).unwrap();
assert_eq!(rendered, expected);
}
#[test]
fn spec_example_2_preserves_jsonld_context() {
let json = r#"{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://trusttasks.org/spec/kyc-handoff/1.0"
],
"id": "urn:uuid:7d8b1e3a-9a72-4f86-9d04-2a4b6c2c5e10",
"type": "https://trusttasks.org/spec/kyc-handoff/1.0",
"issuer": "did:web:verifier.example",
"recipient": "did:web:bank.example",
"issuedAt": "2026-04-12T09:31:00Z",
"payload": {
"subject": "did:key:z6Mk...",
"result": "passed",
"level": "LOA2"
}
}"#;
let doc: TrustTask<KycHandoff> = serde_json::from_str(json).unwrap();
match doc.context.as_ref().unwrap() {
JsonLdContext::Multiple(items) => assert_eq!(items.len(), 2),
other => panic!("expected JsonLdContext::Multiple, got {other:?}"),
}
}
#[test]
fn spec_example_5_error_response() {
let json = r#"{
"id": "9e2a1c44-7b81-4d3e-9b51-7a3c89e3d1f2",
"type": "https://trusttasks.org/spec/trust-task-error/0.1",
"threadId": "4f3c9e2a-1b81-4d3e-9b51-7a3c89e3d1f2",
"issuer": "did:web:bank.example",
"recipient": "did:web:verifier.example",
"issuedAt": "2026-05-16T14:22:00Z",
"payload": {
"code": "expired",
"message": "Task expired at 2026-04-12T09:31:00Z.",
"retryable": false
},
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-rdfc-2022",
"verificationMethod": "did:web:bank.example#key-1",
"created": "2026-05-16T14:22:00Z",
"proofPurpose": "assertionMethod",
"proofValue": "z58D..."
}
}"#;
let doc: TrustTask<ErrorPayload> = serde_json::from_str(json).unwrap();
assert_eq!(doc.type_uri.slug(), "trust-task-error");
assert_eq!(
doc.payload.code,
TrustTaskCode::Standard(StandardCode::Expired)
);
assert!(!doc.payload.retryable);
assert!(doc.proof.is_some());
assert_eq!(
doc.thread_id.as_deref(),
Some("4f3c9e2a-1b81-4d3e-9b51-7a3c89e3d1f2")
);
}
#[test]
fn spec_example_6_extended_error_code() {
let json = r#"{
"id": "c4d2f713-9a8e-4d04-b29c-2f1b0b4cbe71",
"type": "https://trusttasks.org/spec/trust-task-error/0.1",
"threadId": "4f3c9e2a-1b81-4d3e-9b51-7a3c89e3d1f2",
"issuer": "did:web:bank.example",
"recipient": "did:web:verifier.example",
"issuedAt": "2026-05-16T14:22:00Z",
"payload": {
"code": "kyc-handoff:document_revoked",
"message": "Passport used in verification was revoked by the issuing authority on 2026-05-10.",
"retryable": false,
"details": {
"documentRef": "urn:passport:NL:XYZ123456",
"revokedAt": "2026-05-10T08:00:00Z"
}
}
}"#;
let doc: TrustTask<ErrorPayload> = serde_json::from_str(json).unwrap();
match &doc.payload.code {
TrustTaskCode::Extended { slug, local } => {
assert_eq!(slug, "kyc-handoff");
assert_eq!(local, "document_revoked");
}
other => panic!("expected extension code, got {other:?}"),
}
assert!(doc.payload.details.is_some());
}
#[test]
fn in_band_identity_is_authoritative() {
let json = r#"{
"id": "id-1",
"type": "https://trusttasks.org/spec/kyc-handoff/1.0",
"issuer": "did:web:verifier.example",
"payload": {"subject":"s","result":"passed","level":"LOA1"}
}"#;
let doc: TrustTask<KycHandoff> = serde_json::from_str(json).unwrap();
let handler = InMemoryHandler::new()
.with_local("did:web:bank.example")
.with_peer("did:web:verifier.example");
let resolved = handler.resolve_parties(&doc).unwrap();
assert_eq!(resolved.issuer.as_deref(), Some("did:web:verifier.example"));
assert_eq!(resolved.recipient.as_deref(), Some("did:web:bank.example"));
}
#[test]
fn identity_mismatch_is_rejected() {
let json = r#"{
"id": "id-2",
"type": "https://trusttasks.org/spec/kyc-handoff/1.0",
"issuer": "did:web:attacker.example",
"payload": {"subject":"s","result":"passed","level":"LOA1"}
}"#;
let doc: TrustTask<KycHandoff> = serde_json::from_str(json).unwrap();
let handler = InMemoryHandler::new()
.with_local("did:web:bank.example")
.with_peer("did:web:verifier.example");
assert!(handler.resolve_parties(&doc).is_err());
}
#[test]
fn noop_handler_falls_back_to_in_band() {
let json = r#"{
"id": "id-3",
"type": "https://trusttasks.org/spec/kyc-handoff/1.0",
"issuer": "did:web:verifier.example",
"recipient": "did:web:bank.example",
"payload": {"subject":"s","result":"passed","level":"LOA1"}
}"#;
let doc: TrustTask<KycHandoff> = serde_json::from_str(json).unwrap();
let resolved = NoopHandler::new().resolve_parties(&doc).unwrap();
assert_eq!(resolved.issuer.as_deref(), Some("did:web:verifier.example"));
assert_eq!(resolved.recipient.as_deref(), Some("did:web:bank.example"));
}
#[test]
fn expired_document_is_flagged() {
let json = r#"{
"id": "id-4",
"type": "https://trusttasks.org/spec/kyc-handoff/1.0",
"expiresAt": "2026-04-12T09:31:00Z",
"payload": {"subject":"s","result":"passed","level":"LOA1"}
}"#;
let doc: TrustTask<KycHandoff> = serde_json::from_str(json).unwrap();
let now: DateTime<Utc> = "2026-05-01T00:00:00Z".parse().unwrap();
assert!(doc.is_expired_at(now));
}
#[test]
fn response_variant_is_distinct_from_request() {
let request: TypeUri = "https://trusttasks.org/spec/acl/grant/0.1".parse().unwrap();
let response: TypeUri = "https://trusttasks.org/spec/acl/grant/0.1#response"
.parse()
.unwrap();
assert_ne!(request, response);
assert_eq!(request, response.bare());
assert!(response.is_response());
}