@@ -616,7 +616,125 @@ struct DcqlQueryTests {
616616 #expect( result [ " pid_cred " ] ? . count == 4 , " Should have all four claims " )
617617 }
618618
619- }
619+ // MARK: - Structured WalletError.Code tests
620620
621+ @Test ( " WalletError has .claimNotFound code when claim is missing " , arguments: [ " dcql-vehicle " ] )
622+ func testErrorCodeClaimNotFound( dcqlFile: String ) throws {
623+ let dcqlData = try loadTestResource ( fileName: dcqlFile)
624+ let wrapper = try JSONDecoder ( ) . decode ( DCQL . self, from: dcqlData)
625+ let dcql = try DCQL ( credentials: wrapper. credentials)
626+ let dcqlQueryable = DefaultDcqlQueryable (
627+ credentials: [ " cred1 " : ( " org.iso.7367.1.mVRC " , DocDataFormat . cbor) ] ,
628+ claimPaths: [
629+ " cred1 " : [
630+ ClaimPath ( [ . claim( name: " org.iso.7367.1 " ) , . claim( name: " vehicle_holder " ) ] )
631+ // Missing first_name claim
632+ ]
633+ ]
634+ )
635+ do {
636+ _ = try OpenId4VpUtils . resolveDcql ( dcql, queryable: dcqlQueryable)
637+ Issue . record ( " Expected WalletError to be thrown " )
638+ } catch let error as WalletError {
639+ #expect( error. code == . claimNotFound, " Error code should be .claimNotFound " )
640+ #expect( error. context [ " claimPath " ] == " org.iso.18013.5.1/first_name " , " Context should contain the missing claim path " )
641+ }
642+ }
621643
644+ @Test ( " WalletError has .credentialNotFound code when docType is missing " , arguments: [ " dcql-vehicle " ] )
645+ func testErrorCodeCredentialNotFound( dcqlFile: String ) throws {
646+ let dcqlData = try loadTestResource ( fileName: dcqlFile)
647+ let wrapper = try JSONDecoder ( ) . decode ( DCQL . self, from: dcqlData)
648+ let dcql = try DCQL ( credentials: wrapper. credentials)
649+ // No credentials at all
650+ let dcqlQueryable = DefaultDcqlQueryable (
651+ credentials: [ : ] ,
652+ claimPaths: [ : ]
653+ )
654+ do {
655+ _ = try OpenId4VpUtils . resolveDcql ( dcql, queryable: dcqlQueryable)
656+ Issue . record ( " Expected WalletError to be thrown " )
657+ } catch let error as WalletError {
658+ #expect( error. code == . credentialNotFound, " Error code should be .credentialNotFound " )
659+ #expect( error. context [ " docType " ] == " org.iso.7367.1.mVRC " , " Context should contain the missing docType " )
660+ }
661+ }
622662
663+ @Test ( " WalletError has .claimValueMismatch code when value doesn't match " , arguments: [ " dcql-query-values " ] )
664+ func testErrorCodeClaimValueMismatch( dcqlFile: String ) throws {
665+ let dcqlData = try loadTestResource ( fileName: dcqlFile)
666+ let dcql = try JSONDecoder ( ) . decode ( DCQL . self, from: dcqlData)
667+ let dcqlQueryable = DefaultDcqlQueryable (
668+ credentials: [
669+ " pid_cred " : ( " https://credentials.example.com/identity_credential " , DocDataFormat . sdjwt)
670+ ] ,
671+ claimPaths: [
672+ " pid_cred " : [
673+ ClaimPath ( [ . claim( name: " last_name " ) ] ) ,
674+ ClaimPath ( [ . claim( name: " first_name " ) ] ) ,
675+ ClaimPath ( [ . claim( name: " address " ) , . claim( name: " street_address " ) ] ) ,
676+ ClaimPath ( [ . claim( name: " postal_code " ) ] )
677+ ]
678+ ] ,
679+ claimValues: [
680+ " pid_cred " : [
681+ ClaimPath ( [ . claim( name: " last_name " ) ] ) : [ " Smith " ] , // Wrong value
682+ ClaimPath ( [ . claim( name: " postal_code " ) ] ) : [ " 90210 " ]
683+ ]
684+ ]
685+ )
686+ do {
687+ _ = try OpenId4VpUtils . resolveDcql ( dcql, queryable: dcqlQueryable)
688+ Issue . record ( " Expected WalletError to be thrown " )
689+ } catch let error as WalletError {
690+ #expect( error. code == . claimValueMismatch, " Error code should be .claimValueMismatch " )
691+ #expect( error. context [ " claimPath " ] == " last_name " , " Context should contain the mismatched claim path " )
692+ }
693+ }
694+
695+ @Test ( " WalletError has .claimSetNotSatisfied code when no claim_set option works " , arguments: [ " dcql-claim-sets " ] )
696+ func testErrorCodeClaimSetNotSatisfied( dcqlFile: String ) throws {
697+ let dcqlData = try loadTestResource ( fileName: dcqlFile)
698+ let dcql = try JSONDecoder ( ) . decode ( DCQL . self, from: dcqlData)
699+ let dcqlQueryable = DefaultDcqlQueryable (
700+ credentials: [
701+ " pid_cred " : ( " https://credentials.example.com/identity_credential " , DocDataFormat . sdjwt)
702+ ] ,
703+ claimPaths: [
704+ " pid_cred " : [
705+ ClaimPath ( [ . claim( name: " last_name " ) ] ) ,
706+ ClaimPath ( [ . claim( name: " date_of_birth " ) ] )
707+ // Missing locality, region from set 1
708+ // Missing postal_code from set 2
709+ ]
710+ ]
711+ )
712+ do {
713+ _ = try OpenId4VpUtils . resolveDcql ( dcql, queryable: dcqlQueryable)
714+ Issue . record ( " Expected WalletError to be thrown " )
715+ } catch let error as WalletError {
716+ #expect( error. code == . claimSetNotSatisfied, " Error code should be .claimSetNotSatisfied " )
717+ #expect( error. context [ " claimPath " ] != nil , " Context should contain the first missing claim path " )
718+ }
719+ }
720+
721+ @Test ( " WalletError backward compatibility — code and context default to nil and empty " )
722+ func testWalletErrorBackwardCompatibility( ) throws {
723+ let error = WalletError ( description: " Some legacy error " )
724+ #expect( error. code == nil , " Code should default to nil " )
725+ #expect( error. context. isEmpty, " Context should default to empty " )
726+ #expect( error. errorDescription == " Some legacy error " , " Description should still work " )
727+ }
728+
729+ @Test ( " WalletError with code and context preserves all fields " )
730+ func testWalletErrorStructuredFields( ) throws {
731+ let error = WalletError (
732+ description: " Claim not found: family_name_birth " ,
733+ code: . claimNotFound,
734+ context: [ " claimPath " : " family_name_birth " ]
735+ )
736+ #expect( error. code == . claimNotFound)
737+ #expect( error. context [ " claimPath " ] == " family_name_birth " )
738+ #expect( error. errorDescription == " Claim not found: family_name_birth " )
739+ }
740+ }
0 commit comments