From 6a0e1df09017f4c6422275fe7073e68b74a2fb3f Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Fri, 16 Jan 2026 00:41:57 +0000 Subject: [PATCH] More pex to bounds unit tests --- .../data/pex/credential-expiration-pexv2.json | 24 + .../data/pex/credit-score-check-pexv2.json | 45 ++ .../pex/driverlicense-revocation-pexv2.json | 25 + .../pex/gettype-proofofemployment-pexv2.json | 25 + .../pex/gettype-universitydegree-pexv2.json | 25 + .../tests/data/pex/integerrange-pexv2.json | 27 + .../tests/data/pex/invalid-path-pexv2.json | 25 + .../pex/issuedategreaterthan1990-pexv2.json | 25 + .../tests/data/pex/no-path-pexv2.json | 24 + .../data/pex/numbergreaterthan0-pexv2.json | 26 + .../tests/data/pex/numberlessthan-pexv2.json | 26 + .../data/pex/numberrange-multipath-pexv2.json | 28 + .../tests/data/pex/numberrange-pexv2.json | 27 + .../pex/pd-schema-multiple-constraints.json | 58 ++ .../pex/pd-simple-schema-age-predicate.json | 33 ++ .../tests/data/pex/singlegroup-pexv2.json | 57 ++ .../tests/data/pex/twocredentials-pexv2.json | 60 ++ .../data/pex/unidegree-datebetween-pexv2.json | 46 ++ .../pex/unidegree-dategreaterthan-pexv2.json | 35 ++ .../data/pex/unidegree-dateranged-pexv2.json | 36 ++ .../tests/data/pex/unidegree-pexv2.json | 34 ++ .../tests/data/vcs/misccredential.json | 37 ++ .../tests/pex/pex-to-bounds.test.js | 553 ++++++++++++++++++ 23 files changed, 1301 insertions(+) create mode 100644 packages/credential-sdk/tests/data/pex/credential-expiration-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/credit-score-check-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/driverlicense-revocation-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/gettype-proofofemployment-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/gettype-universitydegree-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/integerrange-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/invalid-path-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/issuedategreaterthan1990-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/no-path-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/numbergreaterthan0-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/numberlessthan-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/numberrange-multipath-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/numberrange-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/pd-schema-multiple-constraints.json create mode 100644 packages/credential-sdk/tests/data/pex/pd-simple-schema-age-predicate.json create mode 100644 packages/credential-sdk/tests/data/pex/singlegroup-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/twocredentials-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/unidegree-datebetween-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/unidegree-dategreaterthan-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/unidegree-dateranged-pexv2.json create mode 100644 packages/credential-sdk/tests/data/pex/unidegree-pexv2.json create mode 100644 packages/credential-sdk/tests/data/vcs/misccredential.json create mode 100644 packages/credential-sdk/tests/pex/pex-to-bounds.test.js diff --git a/packages/credential-sdk/tests/data/pex/credential-expiration-pexv2.json b/packages/credential-sdk/tests/data/pex/credential-expiration-pexv2.json new file mode 100644 index 000000000..b653c30c8 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/credential-expiration-pexv2.json @@ -0,0 +1,24 @@ +{ + "presentation_definition": { + "name": "expirationDate less than 2020", + "input_descriptors": [ + { + "id": "drivers_license_information", + "name": "Verify Valid License", + "purpose": "We need you to show that your driver's license will be valid through December of this year.", + "constraints": { + "fields": [ + { + "path": ["$.expirationDate"], + "filter": { + "type": "string", + "format": "date-time", + "formatMaximum": "2020-12-31T23:59:59.000Z" + } + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/packages/credential-sdk/tests/data/pex/credit-score-check-pexv2.json b/packages/credential-sdk/tests/data/pex/credit-score-check-pexv2.json new file mode 100644 index 000000000..ef5869f3e --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/credit-score-check-pexv2.json @@ -0,0 +1,45 @@ +{ + "presentation_definition": { + "name": "Credit Score Check", + "id": "31e2f0f1-6b70-411d-b239-56aed5321884", + "purpose": "Proof that your Credit Score is good: above 800", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "CIBC: Credit Score Check", + "purpose": "Proof that your Credit Score is good: above 800", + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSchema.id" + ], + "filter": { + "const": "https://schema.truvera.io/CreditScore-V1-1732907844039.json" + } + }, + { + "path": [ + "$.credentialSubject.creditScore" + ], + "filter": { + "const": 800 + }, + "predicate": "required" + }, + { + "path": [ + "$.credentialSubject.bankruptcies" + ], + "filter": { + "const": 0 + }, + "optional": false, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/driverlicense-revocation-pexv2.json b/packages/credential-sdk/tests/data/pex/driverlicense-revocation-pexv2.json new file mode 100644 index 000000000..fe11111a6 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/driverlicense-revocation-pexv2.json @@ -0,0 +1,25 @@ +{ + "presentation_definition": { + "id": "31e2f0f1-6b70-411d-b239-56aed5321884", + "purpose": "We need to know that your license has not been revoked.", + "input_descriptors": [ + { + "id": "drivers_license_information", + "name": "Verify Valid License", + "purpose": "We need to know that your license has not been revoked.", + "schema": [ + { + "uri": "https://yourwatchful.gov/drivers-license-schema.json" + } + ], + "constraints": { + "fields": [ + { + "path": ["$.credentialStatus"] + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/gettype-proofofemployment-pexv2.json b/packages/credential-sdk/tests/data/pex/gettype-proofofemployment-pexv2.json new file mode 100644 index 000000000..30827f2b9 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/gettype-proofofemployment-pexv2.json @@ -0,0 +1,25 @@ +{ + "presentation_definition": { + "name": "get type ProofOfEmployment", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "get type ProofOfEmployment", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.type[*]" + ], + "filter": { + "const": "ProofOfEmployment" + } + } + ] + } + } + ] + } +} + diff --git a/packages/credential-sdk/tests/data/pex/gettype-universitydegree-pexv2.json b/packages/credential-sdk/tests/data/pex/gettype-universitydegree-pexv2.json new file mode 100644 index 000000000..5f54f7862 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/gettype-universitydegree-pexv2.json @@ -0,0 +1,25 @@ +{ + "presentation_definition": { + "name": "get type UniversityDegree", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "get type UniversityDegree", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.type[*]" + ], + "filter": { + "const": "UniversityDegree" + } + } + ] + } + } + ] + } +} + diff --git a/packages/credential-sdk/tests/data/pex/integerrange-pexv2.json b/packages/credential-sdk/tests/data/pex/integerrange-pexv2.json new file mode 100644 index 000000000..f5c1b1dc9 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/integerrange-pexv2.json @@ -0,0 +1,27 @@ +{ + "presentation_definition": { + "name": "number range", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "number range", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSubject.number" + ], + "filter": { + "type": "integer", + "exclusiveMaximum": 123123, + "exclusiveMinimum": 0 + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/invalid-path-pexv2.json b/packages/credential-sdk/tests/data/pex/invalid-path-pexv2.json new file mode 100644 index 000000000..1d19aa3a3 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/invalid-path-pexv2.json @@ -0,0 +1,25 @@ +{ + "presentation_definition": { + "name": "number range (invalid path)", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "number range", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": "$.", + "filter": { + "type": "number", + "exclusiveMaximum": 123123, + "exclusiveMinimum": 0 + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/issuedategreaterthan1990-pexv2.json b/packages/credential-sdk/tests/data/pex/issuedategreaterthan1990-pexv2.json new file mode 100644 index 000000000..92370e2f6 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/issuedategreaterthan1990-pexv2.json @@ -0,0 +1,25 @@ +{ + "presentation_definition": { + "name": "issue date greater than 1990", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "issue date greater than 1990", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": ["$.issuanceDate"], + "filter": { + "type": "string", + "format": "date-time", + "formatMinimum": "1990-10-05T14:38:34.852Z" + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/no-path-pexv2.json b/packages/credential-sdk/tests/data/pex/no-path-pexv2.json new file mode 100644 index 000000000..156e4b893 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/no-path-pexv2.json @@ -0,0 +1,24 @@ +{ + "presentation_definition": { + "name": "number range (no path)", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "number range", + "purpose": "test", + "constraints": { + "fields": [ + { + "filter": { + "type": "number", + "exclusiveMaximum": 123123, + "exclusiveMinimum": 0 + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/numbergreaterthan0-pexv2.json b/packages/credential-sdk/tests/data/pex/numbergreaterthan0-pexv2.json new file mode 100644 index 000000000..00cd99a92 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/numbergreaterthan0-pexv2.json @@ -0,0 +1,26 @@ +{ + "presentation_definition": { + "name": "number greater than 0", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "number greater than 0", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSubject.number" + ], + "filter": { + "type": "number", + "exclusiveMinimum": 0 + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/numberlessthan-pexv2.json b/packages/credential-sdk/tests/data/pex/numberlessthan-pexv2.json new file mode 100644 index 000000000..648606af3 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/numberlessthan-pexv2.json @@ -0,0 +1,26 @@ +{ + "presentation_definition": { + "name": "number less than 0", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "number less than 0", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSubject.number" + ], + "filter": { + "type": "number", + "exclusiveMaximum": 123123 + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/numberrange-multipath-pexv2.json b/packages/credential-sdk/tests/data/pex/numberrange-multipath-pexv2.json new file mode 100644 index 000000000..0f5d0b422 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/numberrange-multipath-pexv2.json @@ -0,0 +1,28 @@ +{ + "presentation_definition": { + "name": "number range multi path", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "number range multi path", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSubject.number", + "$.credentialSubject.numberTwo" + ], + "filter": { + "type": "number", + "exclusiveMaximum": 123123, + "exclusiveMinimum": 0 + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/numberrange-pexv2.json b/packages/credential-sdk/tests/data/pex/numberrange-pexv2.json new file mode 100644 index 000000000..36f1a520b --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/numberrange-pexv2.json @@ -0,0 +1,27 @@ +{ + "presentation_definition": { + "name": "number range", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "number range", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSubject.number" + ], + "filter": { + "type": "number", + "exclusiveMaximum": 123123, + "exclusiveMinimum": 0 + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/pd-schema-multiple-constraints.json b/packages/credential-sdk/tests/data/pex/pd-schema-multiple-constraints.json new file mode 100644 index 000000000..b9e34e54f --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/pd-schema-multiple-constraints.json @@ -0,0 +1,58 @@ +{ + "comment": "Note: VP, OIDC, DIDComm, or CHAPI outer wrapper would be here.", + "presentation_definition": { + "id": "31e2f0f1-6b70-411d-b239-56aed5321884", + "purpose": "To sell you a drink we need to know that you are an adult.", + "input_descriptors": [ + { + "id": "867bfe7a-5b91-46b2-9ba4-70028b8d9cc8", + "purpose": "Your age should be greater or equal to 18.", + "schema": [ + { + "uri": "https://www.w3.org/TR/vc-data-model/#types" + } + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.credentialSubject.age", + "$.credentialSubject.details.age" + ], + "filter": { + "type": "integer", + "minimum": 18 + }, + "predicate": "required" + }, { + "path": [ + "$.credentialSubject.citizenship[*]", + "$.credentialSubject.details.citizenship[*]" + ], + "filter": { + "type": "string", + "enum": [ + "eu", + "us", + "uk" + ] + }, + "predicate": "required" + }, { + "path": [ + "$.credentialSubject.country[*].abbr", + "$.credentialSubject.details.country[*].abbr" + ], + "filter": { + "type": "string", + "pattern": "NLD" + }, + "predicate": "required" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/packages/credential-sdk/tests/data/pex/pd-simple-schema-age-predicate.json b/packages/credential-sdk/tests/data/pex/pd-simple-schema-age-predicate.json new file mode 100644 index 000000000..de39d0d83 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/pd-simple-schema-age-predicate.json @@ -0,0 +1,33 @@ +{ + "comment": "Note: VP, OIDC, DIDComm, or CHAPI outer wrapper would be here.", + "presentation_definition": { + "id": "31e2f0f1-6b70-411d-b239-56aed5321884", + "purpose": "To sell you a drink we need to know that you are an adult.", + "input_descriptors": [ + { + "id": "867bfe7a-5b91-46b2-9ba4-70028b8d9cc8", + "purpose": "Your age should be greater or equal to 18.", + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials/v1" + } + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.credentialSubject.age" + ], + "filter": { + "type": "integer", + "minimum": 18 + }, + "predicate": "required" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/packages/credential-sdk/tests/data/pex/singlegroup-pexv2.json b/packages/credential-sdk/tests/data/pex/singlegroup-pexv2.json new file mode 100644 index 000000000..0e13c30ac --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/singlegroup-pexv2.json @@ -0,0 +1,57 @@ +{ + "comment": "VP, OIDC, DIDComm, or CHAPI outer wrapper here", + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "submission_requirements": [{ + "name": "Citizenship Information", + "rule": "pick", + "count": 1, + "from": "A" + }], + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": ["$.issuer", "$.vc.issuer", "$.iss"], + "purpose": "We can only accept digital driver's licenses issued by national authorities of member states or trusted notarial auditors.", + "filter": { + "type": "string", + "pattern": "did:example:gov1|did:example:gov2" + } + }, + { + "path": ["$.credentialSubject.dob", "$.vc.credentialSubject.dob", "$.dob"], + "filter": { + "type": "string", + "format": "date", + "maximum": "1999-06-15" + } + } + ] + } + }, + { + "id": "citizenship_input_2", + "name": "US Passport", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": ["$.credentialSubject.birth_date", "$.vc.credentialSubject.birth_date", "$.birth_date"], + "filter": { + "type": "string", + "format": "date", + "maximum": "1999-05-16" + } + } + ] + } + } + ] + } +} + diff --git a/packages/credential-sdk/tests/data/pex/twocredentials-pexv2.json b/packages/credential-sdk/tests/data/pex/twocredentials-pexv2.json new file mode 100644 index 000000000..7360069f4 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/twocredentials-pexv2.json @@ -0,0 +1,60 @@ +{ + "comment": "NOTE: for multi select to work we need groups and submission_requirements", + "presentation_definition": { + "name": "two credentials", + "submission_requirements": [ + { + "name": "first credential", + "purpose": "show me the money!", + "rule": "pick", + "count": 1, + "from": "1" + }, + { + "name": "second credential", + "purpose": "show me the money!", + "rule": "pick", + "count": 1, + "from": "2" + } + ], + "input_descriptors": [ + { + "id": "Credential 1", + "name": "two credentials uni degree", + "purpose": "test", + "group": ["1"], + "constraints": { + "fields": [ + { + "path": [ + "$.type[*]" + ], + "filter": { + "const": "UniversityDegree" + } + } + ] + } + }, + { + "id": "Credential 2", + "name": "two credentials proof of employment", + "purpose": "test", + "group": ["2"], + "constraints": { + "fields": [ + { + "path": [ + "$.type[*]" + ], + "filter": { + "const": "ProofOfEmployment" + } + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/unidegree-datebetween-pexv2.json b/packages/credential-sdk/tests/data/pex/unidegree-datebetween-pexv2.json new file mode 100644 index 000000000..20d22fc95 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/unidegree-datebetween-pexv2.json @@ -0,0 +1,46 @@ +{ + "presentation_definition": { + "name": "unidegree earned date between", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "unidegree earned date between", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.type[*]" + ], + "filter": { + "const": "UniversityDegree" + } + }, + { + "path": [ + "$.credentialSubject.dateEarned" + ], + "filter": { + "type": "string", + "format": "date", + "formatMinimum": "1999-01-01" + }, + "predicate": "required" + }, + { + "path": [ + "$.credentialSubject.dateEarned" + ], + "filter": { + "type": "string", + "format": "date", + "formatMaximum": "2050-12-31" + }, + "predicate": "required" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/packages/credential-sdk/tests/data/pex/unidegree-dategreaterthan-pexv2.json b/packages/credential-sdk/tests/data/pex/unidegree-dategreaterthan-pexv2.json new file mode 100644 index 000000000..c0e5b35dc --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/unidegree-dategreaterthan-pexv2.json @@ -0,0 +1,35 @@ +{ + "presentation_definition": { + "name": "uni degree date greater than", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "uni degree date greater than", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.type[*]" + ], + "filter": { + "const": "UniversityDegree" + } + }, + { + "path": [ + "$.credentialSubject.dateEarned" + ], + "filter": { + "type": "string", + "format": "date", + "formatMinimum": "1999-01-01" + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/unidegree-dateranged-pexv2.json b/packages/credential-sdk/tests/data/pex/unidegree-dateranged-pexv2.json new file mode 100644 index 000000000..515c60304 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/unidegree-dateranged-pexv2.json @@ -0,0 +1,36 @@ +{ + "presentation_definition": { + "name": "unidegree date range", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "unidegree date range", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.type[*]" + ], + "filter": { + "const": "UniversityDegree" + } + }, + { + "path": [ + "$.credentialSubject.dateEarned" + ], + "filter": { + "type": "string", + "format": "date", + "formatMaximum": "2030-12-31", + "formatMinimum": "2000-01-01" + }, + "predicate": "required" + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/pex/unidegree-pexv2.json b/packages/credential-sdk/tests/data/pex/unidegree-pexv2.json new file mode 100644 index 000000000..a1eb65bd0 --- /dev/null +++ b/packages/credential-sdk/tests/data/pex/unidegree-pexv2.json @@ -0,0 +1,34 @@ +{ + "presentation_definition": { + "name": "Is UniversityDegree and fields exist", + "input_descriptors": [ + { + "id": "Credential 1", + "name": "test degree", + "purpose": "test", + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSubject.id" + ] + }, + { + "path": [ + "$.type[*]" + ], + "filter": { + "const": "UniversityDegree" + } + }, + { + "path": [ + "$.credentialSubject.dateEarned" + ] + } + ] + } + } + ] + } +} diff --git a/packages/credential-sdk/tests/data/vcs/misccredential.json b/packages/credential-sdk/tests/data/vcs/misccredential.json new file mode 100644 index 000000000..ae3045e73 --- /dev/null +++ b/packages/credential-sdk/tests/data/vcs/misccredential.json @@ -0,0 +1,37 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "dk": "https://ld.truvera.io/credentials#", + "UniversityDegree": "dk:UniversityDegree", + "degreeName": "dk:degreeName", + "degreeType": "dk:degreeType", + "dateEarned": "dk:dateEarned", + "name": "dk:name", + "dateOfBirth": "dk:dateOfBirth" + } + ], + "id": "http://127.0.0.1:3001/8b568f80781659d2b07d7fdc0603af1f8cbf3d1c7cfa9558314c9f3215b3053e", + "type": [ + "VerifiableCredential", + "UniversityDegree" + ], + "credentialSubject": { + "id": "did:sam:was:here", + "number": 100 + }, + "expirationDate": "2019-10-05T13:36:15.667Z", + "issuanceDate": "2023-10-05T13:36:15.667Z", + "issuer": { + "name": "key", + "id": "did:key:z6MkqBcwvYurNSSqyBkxavv4fkaq2iu3v3YGMbdyfa4bVNxD" + }, + "name": "Misc Credential", + "proof": { + "type": "Ed25519Signature2018", + "created": "2023-10-05T13:37:06Z", + "verificationMethod": "did:key:z6MkqBcwvYurNSSqyBkxavv4fkaq2iu3v3YGMbdyfa4bVNxD#z6MkqBcwvYurNSSqyBkxavv4fkaq2iu3v3YGMbdyfa4bVNxD", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ROjSBeO3G65lIxQz4Z5VPbilfpmS2IUKt0RqrDuX8oLQvXSSrHo_3nqDVsLhXq22MuS6xsAcOCi9QKFljpVpAA" + } +} diff --git a/packages/credential-sdk/tests/pex/pex-to-bounds.test.js b/packages/credential-sdk/tests/pex/pex-to-bounds.test.js new file mode 100644 index 000000000..71777cabe --- /dev/null +++ b/packages/credential-sdk/tests/pex/pex-to-bounds.test.js @@ -0,0 +1,553 @@ +import { + MAX_NUMBER, + MIN_NUMBER, + EPSILON_NUMBER, + EPSILON_INT, + pexToBounds, +} from '../../src/pex/pex-bounds'; + +import getNumberRange from '../data/pex/numberrange-pexv2.json'; +import getNumberGreaterThan0 from '../data/pex/numbergreaterthan0-pexv2.json'; +import getNumberLessThan from '../data/pex/numberlessthan-pexv2.json'; +import getNumberRangeMutliPath from '../data/pex/numberrange-multipath-pexv2.json'; +import getIntegerRange from '../data/pex/integerrange-pexv2.json'; + +import getUniversityDegreeDateBetween from '../data/pex/unidegree-datebetween-pexv2.json'; +import getUniversityDegreeDateGreaterThan from '../data/pex/unidegree-dategreaterthan-pexv2.json'; +import getUniversityDegreeDateRanged from '../data/pex/unidegree-dateranged-pexv2.json'; + +import noPathPexV2 from '../data/pex/no-path-pexv2.json'; + +import miscCredential from '../data/vcs/misccredential.json'; + +const MAXDATE_STR = '+030000-01-01T03:00:00.000Z'; +const MINDATE_STR = '1412-07-11T17:39:15.585Z'; + +const blankCredential = {}; + +describe('pexToBounds utilities', () => { + describe('basic bounds conversion', () => { + test('getIntegerRange', () => { + const bounds = pexToBounds(getIntegerRange.presentation_definition, [blankCredential]); + expect(bounds).toEqual([ + [ + { + attributeName: 'credentialSubject.number', + format: undefined, + min: EPSILON_INT, + max: 123123 - EPSILON_INT, + type: 'integer', + }, + ], + ]); + }); + + test('getNumberRange', () => { + const bounds = pexToBounds(getNumberRange.presentation_definition, [blankCredential]); + expect(bounds).toEqual([ + [ + { + attributeName: 'credentialSubject.number', + format: undefined, + min: EPSILON_NUMBER, + max: 123123 - EPSILON_NUMBER, + type: 'number', + }, + ], + ]); + }); + + test('getNumberGreaterThan0', () => { + const bounds = pexToBounds(getNumberGreaterThan0.presentation_definition, [blankCredential]); + expect(bounds).toEqual([ + [ + { + attributeName: 'credentialSubject.number', + format: undefined, + min: EPSILON_NUMBER, + max: MAX_NUMBER, + type: 'number', + }, + ], + ]); + }); + + test('getNumberLessThan', () => { + const bounds = pexToBounds(getNumberLessThan.presentation_definition, [blankCredential]); + expect(bounds).toEqual([ + [ + { + attributeName: 'credentialSubject.number', + format: undefined, + min: MIN_NUMBER, + max: 123123 - EPSILON_NUMBER, + type: 'number', + }, + ], + ]); + }); + + test('getNumberRangeMutliPath', () => { + const bounds = pexToBounds(getNumberRangeMutliPath.presentation_definition, [miscCredential]); + expect(bounds).toEqual([ + [ + { + attributeName: 'credentialSubject.number', + format: undefined, + min: EPSILON_NUMBER, + max: 123123 - EPSILON_NUMBER, + type: 'number', + }, + ], + ]); + }); + + test('getUniversityDegreeDateBetween', () => { + const bounds = pexToBounds(getUniversityDegreeDateBetween.presentation_definition, [ + blankCredential, + ]); + expect(JSON.parse(JSON.stringify(bounds))).toEqual([ + [ + { + attributeName: 'credentialSubject.dateEarned', + format: 'date', + min: '1999-01-01T00:00:00.000Z', + max: MAXDATE_STR, + type: 'string', + }, + { + attributeName: 'credentialSubject.dateEarned', + format: 'date', + min: MINDATE_STR, + max: '2050-12-31T00:00:00.000Z', + type: 'string', + }, + ], + ]); + }); + + test('getUniversityDegreeDateGreaterThan', () => { + const bounds = pexToBounds(getUniversityDegreeDateGreaterThan.presentation_definition, [ + blankCredential, + ]); + expect(JSON.parse(JSON.stringify(bounds))).toEqual([ + [ + { + attributeName: 'credentialSubject.dateEarned', + format: 'date', + min: '1999-01-01T00:00:00.000Z', + max: MAXDATE_STR, + type: 'string', + }, + ], + ]); + }); + + test('getUniversityDegreeDateRanged', () => { + const bounds = pexToBounds(getUniversityDegreeDateRanged.presentation_definition, [ + blankCredential, + ]); + // The actual data has formatMinimum/formatMaximum that create specific date bounds + // pexToBounds returns dates as ISO strings when using formatMinimum/formatMaximum + expect(Array.isArray(bounds[0])).toBe(true); + expect(bounds[0][0].attributeName).toBe('credentialSubject.dateEarned'); + }); + + test('no path throws appropriate error', () => { + expect(() => + pexToBounds(noPathPexV2.presentation_definition, [blankCredential]) + ).toThrowError('Missing or empty field "path" property, expected array or string'); + }); + + test('throws for unsupported type/format combination', () => { + const req = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.credentialSubject.custom'], + filter: { type: 'custom-type', format: 'custom-format', minimum: 0 }, + }, + ], + }, + }, + ], + }; + expect(() => pexToBounds(req, [blankCredential])).toThrow( + /Unsupported format custom-format and type custom-type/ + ); + }); + + test('handles legacy embedded schema with data URI', () => { + const credentialWithLegacySchema = { + credentialSchema: { + id: + 'data:application/json;charset=utf-8,' + + encodeURIComponent( + JSON.stringify({ + jsonSchema: { + properties: { + number: { type: 'integer', minimum: 5, maximum: 50 }, + }, + }, + }) + ), + }, + credentialSubject: { number: 25 }, + }; + const req = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.credentialSubject.number'], + filter: { type: 'integer', exclusiveMinimum: 10, exclusiveMaximum: 100 }, + }, + ], + }, + }, + ], + }; + const bounds = pexToBounds(req, [credentialWithLegacySchema]); + expect(bounds[0][0].min).toBe(10 + EPSILON_INT); + expect(bounds[0][0].max).toBe(100 - EPSILON_INT); + }); + }); + + describe('exclusive bounds and edge cases', () => { + it('handles exclusiveMaximum for numbers with epsilon subtraction', () => { + const pex = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.credentialSubject.age'], + filter: { + type: 'number', + exclusiveMaximum: 100, + }, + }, + ], + }, + }, + ], + }; + const bounds = pexToBounds(pex, [blankCredential]); + expect(bounds[0][0].max).toBeLessThan(100); + expect(bounds[0][0].max).toBeCloseTo(100 - EPSILON_NUMBER); + }); + + it('handles exclusiveMinimum for numbers with epsilon addition', () => { + const pex = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.credentialSubject.age'], + filter: { + type: 'number', + exclusiveMinimum: 0, + }, + }, + ], + }, + }, + ], + }; + const bounds = pexToBounds(pex, [blankCredential]); + expect(bounds[0][0].min).toBeGreaterThan(0); + expect(bounds[0][0].min).toBeCloseTo(0 + EPSILON_NUMBER); + }); + + it('handles exclusiveMaximum for integers with epsilon subtraction', () => { + const pex = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.credentialSubject.age'], + filter: { + type: 'integer', + exclusiveMaximum: 100, + }, + }, + ], + }, + }, + ], + }; + const bounds = pexToBounds(pex, [blankCredential]); + expect(bounds[0][0].max).toBe(100 - EPSILON_INT); + }); + + it('handles exclusiveMinimum for integers with epsilon addition', () => { + const pex = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.credentialSubject.age'], + filter: { + type: 'integer', + exclusiveMinimum: 0, + }, + }, + ], + }, + }, + ], + }; + const bounds = pexToBounds(pex, [blankCredential]); + expect(bounds[0][0].min).toBe(0 + EPSILON_INT); + }); + + it('removeFromRequest=true prunes the field from request', () => { + const req = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.credentialSubject.number'], + filter: { type: 'number', minimum: 0, maximum: 10 }, + }, + ], + }, + }, + ], + }; + const fieldsRef = req.input_descriptors[0].constraints.fields; + const bounds = pexToBounds(req, [blankCredential], true); + expect(bounds[0][0]).toMatchObject({ attributeName: 'credentialSubject.number' }); + expect(fieldsRef.length).toBe(0); + }); + + it('handles exclusiveMaximum for numbers', async () => { + const cred = { + credentialSchema: { + details: JSON.stringify({ + jsonSchema: { + properties: { age: { type: 'number', multipleOf: 0.5 } }, + }, + }), + }, + }; + const pex = { + input_descriptors: [ + { + constraints: { + fields: [{ path: ['$.age'], filter: { exclusiveMaximum: 100 } }], + }, + }, + ], + }; + const result = pexToBounds(pex, [cred], false); + expect(result[0][0].max).toBeLessThan(100); + }); + + it('handles exclusiveMinimum for numbers', async () => { + const cred = { + credentialSchema: { + details: JSON.stringify({ + jsonSchema: { + properties: { age: { type: 'number', multipleOf: 0.5 } }, + }, + }), + }, + }; + const pex = { + input_descriptors: [ + { + constraints: { + fields: [{ path: ['$.age'], filter: { exclusiveMinimum: 10 } }], + }, + }, + ], + }; + const result = pexToBounds(pex, [cred], false); + expect(result[0][0].min).toBeGreaterThan(10); + }); + + it('handles exclusiveMaximum for integers', async () => { + const cred = { + credentialSchema: { + details: JSON.stringify({ + jsonSchema: { + properties: { count: { type: 'integer' } }, + }, + }), + }, + }; + const pex = { + input_descriptors: [ + { + constraints: { + fields: [{ path: ['$.count'], filter: { exclusiveMaximum: 50 } }], + }, + }, + ], + }; + const result = pexToBounds(pex, [cred], false); + expect(result[0][0].max).toBe(49); + }); + + it('handles exclusiveMinimum for integers', async () => { + const cred = { + credentialSchema: { + details: JSON.stringify({ + jsonSchema: { + properties: { count: { type: 'integer' } }, + }, + }), + }, + }; + const pex = { + input_descriptors: [ + { + constraints: { + fields: [{ path: ['$.count'], filter: { exclusiveMinimum: 5 } }], + }, + }, + ], + }; + const result = pexToBounds(pex, [cred], false); + expect(result[0][0].min).toBe(6); + }); + + it('handles formatMaximum for date-time', async () => { + const cred = { + credentialSchema: { + details: JSON.stringify({ + jsonSchema: { + properties: { issuanceDate: { type: 'string', format: 'date-time' } }, + }, + }), + }, + }; + const pex = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.issuanceDate'], + filter: { formatMaximum: '2024-12-31T23:59:59Z', format: 'date-time' }, + }, + ], + }, + }, + ], + }; + const result = pexToBounds(pex, [cred], false); + expect(result[0][0].max).toBeInstanceOf(Date); + }); + + it('handles formatMinimum for date-time', async () => { + const cred = { + credentialSchema: { + details: JSON.stringify({ + jsonSchema: { + properties: { issuanceDate: { type: 'string', format: 'date-time' } }, + }, + }), + }, + }; + const pex = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.issuanceDate'], + filter: { formatMinimum: '2020-01-01T00:00:00Z', format: 'date-time' }, + }, + ], + }, + }, + ], + }; + const result = pexToBounds(pex, [cred], false); + expect(result[0][0].min).toBeInstanceOf(Date); + }); + + it('throws error for unsupported format types', async () => { + const cred = { + credentialSchema: { + details: JSON.stringify({ + jsonSchema: { + properties: { unknown: { type: 'object' } }, + }, + }), + }, + }; + const pex = { + input_descriptors: [ + { + constraints: { + fields: [ + { + path: ['$.unknown'], + filter: { format: 'custom-format', type: 'object', maximum: 10 }, + }, + ], + }, + }, + ], + }; + expect(() => pexToBounds(pex, [cred], false)).toThrow(/Unsupported format/); + }); + + it('returns early when both max and min are undefined', async () => { + const cred = { + credentialSchema: { + details: JSON.stringify({ + jsonSchema: { + properties: { field: { type: 'string' } }, + }, + }), + }, + }; + const pex = { + input_descriptors: [ + { + constraints: { + fields: [{ path: ['$.field'], filter: { type: 'string' } }], + }, + }, + ], + }; + const result = pexToBounds(pex, [cred], false); + // Should skip fields without bounds + expect(result[0]).toEqual([]); + }); + + test('removeFromRequest parameter works correctly', () => { + const inputDescriptor = { + constraints: { + fields: [ + { + path: ['$.credentialSubject.number'], + filter: { + type: 'number', + minimum: 0, + }, + }, + ], + }, + }; + const pex = { + input_descriptors: [inputDescriptor], + }; + + const bounds = pexToBounds(pex, [blankCredential], true); + expect(bounds).toBeDefined(); + expect(Array.isArray(bounds)).toBe(true); + }); + }); +});