diff --git a/package.json b/package.json index 12ccc5af2..44acc6ece 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "start:widget": "pnpm --filter web5-quickstart-widgets start", "build": "pnpm run shnip && pnpm --filter site run build", "clear": "pnpm --filter site run clear", - "test": "pnpm run shnip && pnpm vitest run --config vite.config.ts --no-threads && pnpm test:browser && pnpm test:kotlin", + "test": "pnpm run shnip && pnpm vitest run --config vite.config.ts --no-threads && pnpm test:kotlin && pnpm test:browser", "test:watch": "pnpm run shnip && pnpm vitest --config vite.config.ts --watch", "test:browser": "pnpm -r test:browser", "test:versions": "node check-versions.js", diff --git a/site/docs/tbdex/wallet/allowlist-pfis.mdx b/site/docs/tbdex/wallet/allowlist-pfis.mdx index d7ac3066a..833555491 100644 --- a/site/docs/tbdex/wallet/allowlist-pfis.mdx +++ b/site/docs/tbdex/wallet/allowlist-pfis.mdx @@ -1,7 +1,14 @@ --- +title: Allowlisting PFIs +hide_title: true sidebar_position: 4 --- +import isPFIJs from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/tbdex/wallet/isPFIJs.snippet.js' +import isPFIKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/tbdex/wallet/isPFIKt.snippet.kt' + + + # Allowlisting PFIs While tbDEX is a permissionless network where any PFI can participate, you may want to curate your own list of PFIs in which you engage with. We refer to this as **allowlisting**. @@ -18,9 +25,12 @@ Consider factors like transaction fees, currency pairs offered, and reputation. To determine if a PFI is part of the tbDEX network, you can check their [DID document](/docs/web5/learn/did_document) for the `PFI` service type: -```js -const isPFI = pfiDid.document.service.some(service => service.type === 'PFI'); -``` + ## Verify PFI Credentials diff --git a/site/docs/tbdex/wallet/send-rfq.mdx b/site/docs/tbdex/wallet/send-rfq.mdx index ba5bfff58..f297202c1 100644 --- a/site/docs/tbdex/wallet/send-rfq.mdx +++ b/site/docs/tbdex/wallet/send-rfq.mdx @@ -8,7 +8,7 @@ After your customer selects an Offering, the next step is to send a **Request fo The Offering provided an estimate, but the Quote will provide a formal offer based on the financial transaction the customer is requesting as well as the payment methods provided. ## Create the RFQ -An RFQ is a structured tbDEX message that specifies the details of the transaction your customer wishes to make. +An RFQ is a structured tbDEX message that specifies the details of the transaction your customer wishes to make. It includes information about the Offering, payment methods, and any required [verifiable credentials (VCs)](/docs/tbdex/wallet/managing-credentials). To create the message, call `Rfq.create()` passing `metadata` and `data`: @@ -58,7 +58,7 @@ const rfq = Rfq.create({ kind: 'BTC_WALLET_ADDRESS', // The method of payment paymentDetails: { btcAddress: BTC_ADDRESS // Customer's BTC wallet address - } + } }, payoutMethod: { kind: 'DEBIT_CARD', // The method for receiving payout @@ -84,10 +84,13 @@ await rfq.sign(customer); // Customer's PortableDid ``` ## Send RFQ to PFI -Use `TbdexHttpClient` to send the RFQ to the PFI: +Use `TbdexHttpClient` to send the RFQ to the PFI. With an optional `replyTo` property containing a valid URI where new messages from the PFI will be sent: ```js -const rfqResponse = await TbdexHttpClient.sendMessage({ message: rfq }); +const rfqResponse = await TbdexHttpClient.sendMessage({ + message: rfq, + replyTo: 'https://tbdex.io/callback' +}); ``` The response contains a `status`, where success would be `202`: diff --git a/site/docs/web5/api_guide.mdx b/site/docs/web5/api_guide.mdx deleted file mode 100644 index 7c9d44044..000000000 --- a/site/docs/web5/api_guide.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -sidebar_position: 1 -title: API Reference ---- - -import {Redirect} from '@docusaurus/router'; - -const Home = () => { - return ; -}; diff --git a/site/docs/web5/build/verifiable-credentials/fan-club-vc.mdx b/site/docs/web5/build/verifiable-credentials/fan-club-vc.mdx index 34b3864c5..21b27ca7e 100644 --- a/site/docs/web5/build/verifiable-credentials/fan-club-vc.mdx +++ b/site/docs/web5/build/verifiable-credentials/fan-club-vc.mdx @@ -4,6 +4,26 @@ hide_title: true sidebar_position: 10 --- +import createFanClubVc from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/createFanClubVc.snippet.js' +import signFanClubVc from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/signFanClubVc.snippet.js' +import satisfiesPresentationDefinitionFanClubVc from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/satisfiesPresentationDefinitionFanClubVc.snippet.js' +import createPresentationFromCredentialsFanClubVc from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/createPresentationFromCredentialsFanClubVc.snippet.js' +import verifyFanClubVc from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/verifyFanClubVc.snippet.js' +import parseFanClubJwt from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/parseFanClubJwt.snippet.js' +import createDids from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/createDids.snippet.js' +import createAndValidatePresentation from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/createAndValidatePresentation.snippet.js' + +import createFanClubVcKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/createFanClubVcKt.snippet.kt' +import signFanClubVcKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/signFanClubVcKt.snippet.kt' +import satisfiesPresentationDefinitionFanClubVcKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/satisfiesPresentationDefinitionFanClubVcKt.snippet.kt' +import createPresentationFromCredentialsFanClubVcKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/createPresentationFromCredentialsFanClubVcKt.snippet.kt' +import verifyFanClubVcKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/verifyFanClubVcKt.snippet.kt' +import parseFanClubJwtKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/parseFanClubJwtKt.snippet.kt' +import createDidsKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/createDidsKt.snippet.kt' +import createAndValidatePresentationKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/createAndValidatePresentationKt.snippet.kt' + + + # Verifiable Credentials for Fan Club Membership Alice is a long-time fan of one of the biggest pop stars in the world! @@ -12,12 +32,62 @@ In this tutorial, we'll learn how to use [verifiable credentials](/docs/web5/lea ![Fan Club Membership](/img/vc-fanclub.png) ## Setting the Stage -Before we start issuing and verifying credentials, let's set up our environment by installing the [@web5/credentials](https://www.npmjs.com/package/@web5/credentials) and [@web5/dids](https://www.npmjs.com/package/@web5/dids) packages: +Before we start issuing and verifying credentials, let's set up our environment by installing the and packages: -```bash + + +### Import packages + +After installing the packages, import them into your project as shown below: + + ## The Cast There are a few characters involved in our story: @@ -29,38 +99,60 @@ There are a few characters involved in our story: ## Act 1: Issuing the Credential In this section, we'll walk through all of the steps required to create and issue a verifiable credential. - + ### Create DIDs As a prerequisite, both the Fan Club and Alice need [decentralized identitifiers](/docs/web5/learn/decentralized-identifiers). This is because verifiable credentials use DIDs to identify issuers and holders. -If you need to create DIDs, you can do so with the [@web5/dids](https://www.npmjs.com/package/@web5/dids) package: +If you need to create DIDs, you can do so with the package: -```js -import { DidKeyMethod } from '@web5/dids'; - -const fanClubIssuerDid = await DidKeyMethod.create(); -const aliceDid = await DidKeyMethod.create(); -``` + ### Model Credential Now, let's create the **SwiftiesFanClub** credential. We'll first model it with a class with some basic properties, `level` and `legit`: -```js title="Issuer.js" -import { VerifiableCredential, PresentationExchange } from "@web5/credentials"; - -class SwiftiesFanClub { - constructor(level, legit) { + :::note Note that credentials can be customized to fit various needs and scenarios. In our example, we used `level` and `legit`, but you could add more properties like `memberSince`, `favoriteAlbum`, or any other relevant data that suits your use case. @@ -70,14 +162,12 @@ Note that credentials can be customized to fit various needs and scenarios. In o Next we can create the verifiable credential with an instance of the class: -```js title="Issuer.js" -const vc = await VerifiableCredential.create({ - type: 'SwiftiesFanClub', - issuer: fanClubIssuerDid.did, - subject: aliceDid.did, - data: new SwiftiesFanClub('Stan', true) -}); -``` + ### Credential Properties @@ -96,7 +186,10 @@ Let's break down the `VerifiableCredential.create()` method to understand what e `VerifiableCredential.create()` returns a VC as a JSON object: -```js + + ### Sign Credential Last but not least, we must cryptographically sign the VC to make it official: -```js title="Issuer.js" -const signedVcJwt = await vc.sign({ did: fanClubIssuerDid }); -``` + + Signing the credential returns a [VC JSON Web Token](/docs/web5/build/verifiable-credentials/jwt-to-vc#what-is-a-jwt), ideal for secure transmission of the credential. @@ -140,40 +265,52 @@ A presentation exchange is the process where the holder of VCs presents them to ### Create Presentation Definition To kick off the presentation exchange, the Swag Supplier (verifier) will send a request to Alice, which includes a presentation definition, specifying what credentials or claims are needed. -Here's how the verifier can create the presentation definition, and validate that it is properly formed: -```js title="Verifier.js" -import { VerifiableCredential, PresentationExchange } from "@web5/credentials"; -const presentationDefinition = { - 'id' : 'presDefId123', - 'name' : 'Swifties Fan Club Presentation Definition', - 'purpose' : 'for proving membership in the fan club', - 'input_descriptors' : [ +To create and validate this presentation definition, the verifier first needs to import packages from the library. + + + +Here's how the verifier can create the presentation definition, and validate that it is properly formed: + + -const definitionValidation = PresentationExchange.validateDefinition({ presentationDefinition }); -``` -A successful validation would result in: + -```js -[ Checked { tag: 'root', status: 'info', message: 'ok' } ] -``` ### Satisfy Presentation Definition @@ -181,29 +318,30 @@ Once a presentation definition is presented to Alice, she'd want to make sure th This can be done by passing the JWT(s) and the presentation definition to `satisfiesPresentationDefinition()` method. Typically this step is done by the app that Alice is using to store and present her credentials: -```js title="Wallet.js" -// Does VC Satisfy the Presentation Definition -try { - PresentationExchange.satisfiesPresentationDefinition({vcJwts: [signedVcJwt], presentationDefinition: presentationDefinition}); - console.log('\nVC Satisfies Presentation Definition!\n'); -} catch (err) { - console.log('VC does not satisfy Presentation Definition: ' + err.message); -} -``` + ### Create Presentation Once Alice's app is confident that her credential satisfies the presentation definition, the presentation would be created: -```js title="Wallet.js" -// Create Presentation Result that contains a Verifiable Presentation and Presentation Submission -const presentationResult = PresentationExchange.createPresentationFromCredentials({vcJwts: [signedVcJwt], presentationDefinition: presentationDefinition }); -console.log('\nPresentation Result: ' + JSON.stringify(presentationResult)); -``` + The result of the presentation: -```json + + ## Act 3: Verifying the Credential After receiving the credential, the verifier will want to verify it to ensure that it is valid and has not been tampered with. They can do this with `VerifiableCredential.verify()`. If verification is unsuccessful, an error will be thrown: -```js title="Verifier.js" -try { - await VerifiableCredential.verify({ vcJwt: signedVcJwt }); - console.log('\nVC Verification successful!\n'); -} catch (err) { - console.log('\nVC Verification failed: ' + err.message + '\n'); -} -``` + + ### Parse JWT into VC @@ -265,13 +437,20 @@ The signed VC JWT is a secure representation of the credential, ideal for transm However, the verifier will likely need to inspect or use the data within the credential. They can use `parseJwt()` to convert the JWT back into a `VerifiableCredential` object. -```js title="Verifier.js" -const parsedVC = await VerifiableCredential.parseJwt( { vcJwt: signedVcJwt }); -``` + + This returns: -```js + + Given Alice's verified credential, she's all set to receive her free merch for being a proud member of the Swifties Fan Club! \ No newline at end of file diff --git a/site/docs/web5/build/verifiable-credentials/presentation-exchange.mdx b/site/docs/web5/build/verifiable-credentials/presentation-exchange.mdx index e1462bc8b..e4e151275 100644 --- a/site/docs/web5/build/verifiable-credentials/presentation-exchange.mdx +++ b/site/docs/web5/build/verifiable-credentials/presentation-exchange.mdx @@ -1,8 +1,25 @@ --- +title: Presentation Exchange +hide_title: true sidebar_position: 3 --- -import CodeSnippet from '@site/src/components/CodeSnippet'; + +import getLoanAppPresentationDefinition from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/getLoanAppPresentationDefinition.snippet.js' +import satisfiesPresentationDefinitionForPex from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/satisfiesPresentationDefinitionForPex.snippet.js' +import selectCredentialsForPex from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/selectCredentialsForPex.snippet.js' +import createPresentationFromCredentialsForPex from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/createPresentationFromCredentialsForPex.snippet.js' +import validVerifiablePresentationForPex from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/validVerifiablePresentationForPex.snippet.js' +import validPresentationSubmissionForPex from '!!raw-loader!@site/snippets/testsuite-javascript/__tests__/web5/build/verifiable-credentials/validPresentationSubmissionForPex.snippet.js' + + +import getLoanAppPresentationDefinitionKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/getLoanAppPresentationDefinitionKt.snippet.kt' +import satisfiesPresentationDefinitionForPexKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/satisfiesPresentationDefinitionForPexKt.snippet.kt' +import selectCredentialsForPexKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/selectCredentialsForPexKt.snippet.kt' +import createPresentationFromCredentialsForPexKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/createPresentationFromCredentialsForPexKt.snippet.kt' +import validVerifiablePresentationForPexKt from '!!raw-loader!@site/snippets/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/validVerifiablePresentationForPexKt.snippet.kt' + + # Presentation Exchange @@ -26,7 +43,12 @@ Assume a lender has an existing Presentation Definition specifying that applican
Presentation Definition - +
@@ -37,9 +59,24 @@ We'll use the Presentation Definition above to present credentials that meet its All methods needed to perform a PEX are in the `PresentationExchange` class. You can import this class from the `@web5/credentials` package: -```js + ## Select Credentials @@ -47,13 +84,23 @@ A user may possess multiple credentials. To preserve the user's privacy, when creating a Presentation, you'll want to present only the VCs that match the Presentation Definition requirements. This is done with the `selectCredentials()` method: - + ## Satisfy Presentation Definition To ensure that the selected credentials collectively satisfy all of the Presentation Definition's requirements, call the `satisfiesPresentationDefinition()` method: - + This method will throw an error if all requirements of the Presentation Definition are not satisfied. @@ -61,11 +108,20 @@ This method will throw an error if all requirements of the Presentation Definiti Once you've confirmed that the VCs successfully satisfy the Presentation Definition, you can create a presentation using the `createPresentationFromCredentials()` method: - + -This method returns a `PresentationResult` which contains the presentation: -```js + + + ## Validate Presentation Submission Before submitting the presentation to a lender, you can validate the Presentation via the `validateSubmission()` method: - + This method checks the presentation submission for any errors, returns an array of `Checked` objects representing the validation checks performed on the presentation submission: @@ -125,6 +238,12 @@ Each `Checked` object contains: ## Verifiable Presentation -Once the [Presentation](/docs/glossary#verifiable-presentation) is error-free and has passed the validation checks, you can submit the Verifiable Presentation by providing the lender with the `presentation` JSON object: - + + + diff --git a/site/src/components/CodeSnippet.jsx b/site/src/components/CodeSnippet.jsx index 221bd19f0..8ee523be1 100644 --- a/site/src/components/CodeSnippet.jsx +++ b/site/src/components/CodeSnippet.jsx @@ -7,6 +7,7 @@ const CodeSnippet = ({ importString, snippet, language = 'javascript', + title }) => { let finalSnippet; @@ -27,7 +28,7 @@ const CodeSnippet = ({ finalSnippet = snippet; } - return {finalSnippet}; + return {finalSnippet}; }; export default CodeSnippet; diff --git a/site/src/components/language/Shnip.jsx b/site/src/components/language/Shnip.jsx index f8f7a72e8..3e2f8f86f 100644 --- a/site/src/components/language/Shnip.jsx +++ b/site/src/components/language/Shnip.jsx @@ -3,50 +3,61 @@ import LanguageSwitchBlock from '@site/src/components/language/LanguageSwitchBlo import LanguageTabBar from '@site/src/components/language/LanguageTabBar'; import CodeSnippet from '@site/src/components/CodeSnippet'; import CodeBlock from '@theme/CodeBlock'; +import ReactMarkdown from 'react-markdown'; -const Shnip = ({ snippets, inlineSnippets }) => { +const Shnip = ({ snippets, inlineSnippets, inlineContent }) => { // support line breaks for inline code snippets - const addLineBreaks = (code, breakLines) => { - if (!breakLines || breakLines.length === 0) { - return code; +const addLineBreaks = (code, breakLines) => { + if (!breakLines || breakLines.length === 0) { + return code.trim(); + } + let lines = code.trim().split('\n'); + if (lines[0] === '') { + lines.shift(); + breakLines = breakLines.map((line) => line - 1); + } + breakLines.forEach((lineNumber) => { + let adjustedLineNumber = lineNumber - 1; + if (adjustedLineNumber >= 0 && adjustedLineNumber < lines.length) { + lines[adjustedLineNumber] += '\n'; } + }); + + return lines.join('\n'); +}; - const lines = code.split('\n'); - breakLines.forEach((lineNumber) => { - if (lineNumber < lines.length) { - lines[lineNumber] += '\n'; - } - }); - return lines.join('\n'); - }; return ( <> {snippets && - snippets.map(({ snippetContent, language, title }) => ( -
- -
- ))} + snippets.map( + ({ snippetContent, language, title, content }, index) => ( +
+ {content && {content}} + +
+ ), + )} {inlineSnippets && inlineSnippets.map( - ({ code, language, codeLanguage, title, breakLineAt }) => ( -
- + ({ content, code, language, codeLanguage, breakLineAt, title }, index) => ( +
+ {content && {content}} + {code && + {addLineBreaks(code, breakLineAt)} - + }
), )} diff --git a/site/testsuites/testsuite-javascript/__tests__/tbdex/wallet/allowListPfi.test.js b/site/testsuites/testsuite-javascript/__tests__/tbdex/wallet/allowListPfi.test.js new file mode 100644 index 000000000..47975a377 --- /dev/null +++ b/site/testsuites/testsuite-javascript/__tests__/tbdex/wallet/allowListPfi.test.js @@ -0,0 +1,25 @@ +import { test, expect, describe, beforeAll } from 'vitest'; +import { DidDhtMethod } from '@web5/dids'; + +let pfiDid; + +describe('allowlist PFIs', () => { + beforeAll(async () => { + pfiDid = await DidDhtMethod.create({ + publish: false, + services: [{ + id: 'pfi', + type: 'PFI', + serviceEndpoint: 'tbdex-pfi.tbddev.org' + }] + }) + }); + + test('PFI DID has PFI service', async () => { + // :snippet-start: isPFIJs + const isPFI = pfiDid.document.service.some(service => service.type === 'PFI'); + // :snippet-end: + expect(isPFI).toBe(true) + }); + +}); \ No newline at end of file diff --git a/site/testsuites/testsuite-javascript/__tests__/web5/build/verifiable-credentials/fan-club-vc.test.js b/site/testsuites/testsuite-javascript/__tests__/web5/build/verifiable-credentials/fan-club-vc.test.js new file mode 100644 index 000000000..3314fc17d --- /dev/null +++ b/site/testsuites/testsuite-javascript/__tests__/web5/build/verifiable-credentials/fan-club-vc.test.js @@ -0,0 +1,175 @@ +import { test, expect, vi, describe, beforeAll } from 'vitest'; +import { DidKeyMethod } from '@web5/dids'; +import { VerifiableCredential, PresentationExchange } from '@web5/credentials'; +import { setUpWeb5 } from '../../../setup-web5'; + +describe('fan-club-vc', () => { + let web5, did, fanClubIssuerDid, aliceDid, SwiftiesFanClub, vc, signedVcJwt, presentationDefinition; + + beforeAll(async () => { + await setUpWeb5(); + web5 = globalThis.web5; + did = globalThis.did; + + fanClubIssuerDid = await DidKeyMethod.create(); + aliceDid = await DidKeyMethod.create(); + + SwiftiesFanClub = class { + constructor(level, legit) { + this.level = level; + this.legit = legit; + } + }; + + vc = await VerifiableCredential.create({ + type: 'SwiftiesFanClub', + issuer: fanClubIssuerDid.did, + subject: aliceDid.did, + data: new SwiftiesFanClub('Stan', true) + }); + + signedVcJwt = await vc.sign({ did: fanClubIssuerDid }); + + presentationDefinition = { + 'id': 'presDefId123', + 'name': 'Swifties Fan Club Presentation Definition', + 'purpose': 'for proving membership in the fan club', + 'input_descriptors': [ + { + 'id': 'legitness', + 'purpose': 'are you legit or not?', + 'constraints': { + 'fields': [ + { + 'path': [ + '$.credentialSubject.legit', + ] + } + ] + } + } + ] + }; + + vi.mock('@web5/api', () => ({ + Web5: { + connect: vi.fn(() => ({ web5, did })), + }, + })); + }); + test('createDids creates an issuer DID and alice DID with did:key method', async () => { + // :snippet-start: createDids + const fanClubIssuerDid = await DidKeyMethod.create(); + const aliceDid = await DidKeyMethod.create(); + // :snippet-end: + expect(aliceDid.did).toMatch(/^did:key:/); + expect(fanClubIssuerDid.did).toMatch(/^did:key:/); + }); + + test('createFanClubVc creates a vc for fan club', async () => { + // :snippet-start: createFanClubVc + const vc = await VerifiableCredential.create({ + type: 'SwiftiesFanClub', + issuer: fanClubIssuerDid.did, + subject: aliceDid.did, + data: new SwiftiesFanClub('Stan', true) + }); + // :snippet-end: + expect(vc).toHaveProperty('vcDataModel'); + expect(vc.vcDataModel).toHaveProperty('credentialSubject'); + expect(vc.vcDataModel.credentialSubject.level).toBe('Stan'); + expect(vc.vcDataModel.credentialSubject.legit).toBe(true); + }); + + test('signFanClubVc signs a vc for fan club and returns jwt', async () => { + // :snippet-start: signFanClubVc + const signedVcJwt = await vc.sign({ did: fanClubIssuerDid }); + // :snippet-end: + expect(typeof signedVcJwt).toBe('string'); + expect(signedVcJwt).not.toBe(''); + }); + + test('createAndValidatePresentation creates and validates presentation definition', async () => { + // :snippet-start: createAndValidatePresentation + const presentationDefinition = { + 'id': 'presDefId123', + 'name': 'Swifties Fan Club Presentation Definition', + 'purpose': 'for proving membership in the fan club', + 'input_descriptors': [ + { + 'id': 'legitness', + 'purpose': 'are you legit or not?', + 'constraints': { + 'fields': [ + { + 'path': [ + '$.credentialSubject.legit', + ] + } + ] + } + } + ] + }; + + const definitionValidation = PresentationExchange.validateDefinition({ presentationDefinition }); + // :snippet-end: + + expect(Array.isArray(definitionValidation)).toBe(true); + expect.soft(definitionValidation[0]).toHaveProperty('status', 'info'); + expect.soft(definitionValidation[0]).toHaveProperty('message', 'ok'); + }); + + test('satisfiesPresentationDefinitionFanClubVc checks if VC satisfies the presentation definition', async () => { + const logSpy = vi.spyOn(console, 'log'); + // :snippet-start: satisfiesPresentationDefinitionFanClubVc + // Does VC Satisfy the Presentation Definition + try { + PresentationExchange.satisfiesPresentationDefinition({ vcJwts: [signedVcJwt], presentationDefinition: presentationDefinition }); + console.log('\nVC Satisfies Presentation Definition!\n'); + } catch (err) { + console.log('VC does not satisfy Presentation Definition: ' + err.message); + } + // :snippet-end: + expect(logSpy).toHaveBeenCalledWith('\nVC Satisfies Presentation Definition!\n'); + logSpy.mockRestore(); + }); + + test('createPresentationFromCredentialsFanClubVc creates presentation from credentials and checks the presentation result', async () => { + // :snippet-start: createPresentationFromCredentialsFanClubVc + // Create Presentation Result that contains a Verifiable Presentation and Presentation Submission + const presentationResult = PresentationExchange.createPresentationFromCredentials({ vcJwts: [signedVcJwt], presentationDefinition: presentationDefinition }); + console.log('\nPresentation Result: ' + JSON.stringify(presentationResult)); + // :snippet-end: + + expect(presentationResult.presentation).toHaveProperty('@context'); + expect(presentationResult.presentation).toHaveProperty('type'); + expect(presentationResult.presentation).toHaveProperty('presentation_submission'); + expect(presentationResult).toHaveProperty('presentationSubmissionLocation'); + expect(presentationResult).toHaveProperty('presentationSubmission'); + }); + test('verifyFanClubVc checks if VC verification is successful', async () => { + const logSpy = vi.spyOn(console, 'log'); + // :snippet-start: verifyFanClubVc + try { + await VerifiableCredential.verify({ vcJwt: signedVcJwt }); + console.log('\nVC Verification successful!\n'); + } catch (err) { + console.log('\nVC Verification failed: ' + err.message + '\n'); + } + // :snippet-end: + expect(logSpy).toHaveBeenCalledWith('\nVC Verification successful!\n'); + logSpy.mockRestore(); + }); + + test('parseFanClubJwt parses the signed VC JWT', async () => { + // :snippet-start: parseFanClubJwt + const parsedVC = await VerifiableCredential.parseJwt({ vcJwt: signedVcJwt }); + // :snippet-end: + expect(parsedVC).toHaveProperty('vcDataModel'); + expect(parsedVC.vcDataModel).toHaveProperty('credentialSubject'); + expect(parsedVC.vcDataModel.credentialSubject.level).toBe('Stan'); + expect(parsedVC.vcDataModel.credentialSubject.legit).toBe(true); + }); +}); + diff --git a/site/testsuites/testsuite-javascript/__tests__/web5/build/verifiable-credentials/presentation-exchange.test.js b/site/testsuites/testsuite-javascript/__tests__/web5/build/verifiable-credentials/presentation-exchange.test.js index 32ef2ba3a..e46f89ba2 100644 --- a/site/testsuites/testsuite-javascript/__tests__/web5/build/verifiable-credentials/presentation-exchange.test.js +++ b/site/testsuites/testsuite-javascript/__tests__/web5/build/verifiable-credentials/presentation-exchange.test.js @@ -1,24 +1,148 @@ import { test, describe, expect } from 'vitest'; import { - pex_checkPresentationDefinitionSatisfaction, pex_createPresentationFromCredentials, pex_getLoanAppPresentationDefinition, - pex_getPresentationFromResult, - pex_selectCredentials, pex_submissionCheck, } from '../../../../../../code-snippets/web5/build/verifiable-credentials/presentation-exchange'; +import { PresentationExchange } from '@web5/credentials'; -const signedEmploymentVcJwt = - 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyI3o2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJFbXBsb3ltZW50Q3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjcyNDhiOTkyLTkwOTYtNDk2NS1hMGVjLTc3ZDhhODNhMWRmYiIsImlzc3VlciI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDoxMToyNVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlEMTR4UmY0cTJNWlh1ZWY2X2ZXYnBGbVlTUG94dGFxTkp1SmdEMG96Wl84UTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWVubGFNbVYzTlhKeVVXdFVjbUV3WlZsVk16WlBTblJzTURCbFJWZHhhalZhV0dkNmNEZFpSVTVKUVNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpQZDJZMFQyMUViamxKWm5SNFdYWnBkRTFHWm1jMVVXeDVMVVV6VWs1b1dsUkdPVlpFTWtnNVQzVjNJaXdpZVNJNkltUnZjVmxtV2s1c1NtRlRNVll4U201bU9HdEZObEF6VkRsd2QzaDNla3hFVTJWc1ZqTlRUa2s1U2xFaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xEWm05bVFUQkpVbU5uY2tWdVVHZHdRbU5RV1ZsV2VFWlliR0pTYjJRd2RVNWZRVkJwTkVrNUxVRmZRU0o5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFFtd3pWWG80VldGT2REZGxlREJKYjJJMFJFNXNhbFJGVmpaelQwTmtjbFJ3TWxvNE5FTkJPVFJPUWtFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVOWk9WRldZbWRKYkUxemRraEZYMVJtTld4a1MxQjBkR3d3WVV4blNrdHNSbmt6Vms0d2QzQTJhVFpSSW4xOSIsImVtcGxveW1lbnRTdGF0dXMiOiJlbXBsb3llZCJ9fX0.Sazc8Ndhs-NKjxvtVMKeC9dxjEkI26fVsp2kFNWM-SYLtxMzKvl5ffeWd81ysHgPmBBSk2ar4dMqGgUsyM4gAQ'; -const signedNameAndDobVcJwt = - 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0I3o2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJOYW1lQW5kRG9iQ3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjliZjM2YzY5LTI0ODAtNDllZC1iMTYyLTRlZDEwOWE3MTc3NyIsImlzc3VlciI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0IiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDowNjowMVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlDS2o2M0FyZlBGcEpsb2lTd3gxQUhxVWtpWlNoSDZGdnZoSzRaTl9fZDFtQTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWNscFdXbTVJVkVrNWFEWkJUVmxVV0dwT01HcFhTVkYwTTI5ak4xTnJTeTF4Y2kxcVVuSTBUalEzUlNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpaVDFwRE5WSmlUMHQ1T0dadVVUWTJVWEZPUkc5aldFMXZPVXhUZEdNNVYyOWthMHd0ZFZCZlExQnZJaXdpZVNJNklsWnZZM0UxVERodFozQlhXVTFrYjFwS1JrWlJUa1ZDT0hsR0xXTndkRWQzZFdkcFRWVm5hR2t6Y21jaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xCTXpSMlMzb3llVmswZVV4dGRDMUdabkJuYWpWbGFFRm1ZWFI1YzFOa2MwNVNWbVpMYkhwUWRqTjVkeUo5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFF6ZGZjMXBzTW1wMVVXNUdhRVJIV1RSb2NFVTRiMlF4YVU5MWRuZG1PVFJ5TVVkbk9HMWFWbVJCVmxFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVKdU5sTnJiSEpWYzNKdVFuaFJPVXBqVXkxTlNVaGtWelYwTXpRM1MxWjNaMXBwVEZwMFQwcDRRVkYzSW4xOSIsIm5hbWUiOiJhbGljZSBib2IiLCJkYXRlT2ZCaXJ0aCI6IjEwLTAxLTE5OTAifX19.mNCDv_JntH-wZpYONKNL58UbOWaYXCYJO_HPI_WVlSgwzo6dhYmV_9qtpFKd_exFb-aaEYPeSE43twWlrJeSBg'; -const credentials = [signedEmploymentVcJwt, signedNameAndDobVcJwt]; const pd = await pex_getLoanAppPresentationDefinition(); describe('Presentation Exchange Process', () => { - test('selectCredentials() selects VCs that match presentation defintion', async () => { - const selectedCredentials = await pex_selectCredentials(credentials, pd); + const signedEmploymentVcJwt = + 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyI3o2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJFbXBsb3ltZW50Q3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjcyNDhiOTkyLTkwOTYtNDk2NS1hMGVjLTc3ZDhhODNhMWRmYiIsImlzc3VlciI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDoxMToyNVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlEMTR4UmY0cTJNWlh1ZWY2X2ZXYnBGbVlTUG94dGFxTkp1SmdEMG96Wl84UTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWVubGFNbVYzTlhKeVVXdFVjbUV3WlZsVk16WlBTblJzTURCbFJWZHhhalZhV0dkNmNEZFpSVTVKUVNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpQZDJZMFQyMUViamxKWm5SNFdYWnBkRTFHWm1jMVVXeDVMVVV6VWs1b1dsUkdPVlpFTWtnNVQzVjNJaXdpZVNJNkltUnZjVmxtV2s1c1NtRlRNVll4U201bU9HdEZObEF6VkRsd2QzaDNla3hFVTJWc1ZqTlRUa2s1U2xFaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xEWm05bVFUQkpVbU5uY2tWdVVHZHdRbU5RV1ZsV2VFWlliR0pTYjJRd2RVNWZRVkJwTkVrNUxVRmZRU0o5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFFtd3pWWG80VldGT2REZGxlREJKYjJJMFJFNXNhbFJGVmpaelQwTmtjbFJ3TWxvNE5FTkJPVFJPUWtFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVOWk9WRldZbWRKYkUxemRraEZYMVJtTld4a1MxQjBkR3d3WVV4blNrdHNSbmt6Vms0d2QzQTJhVFpSSW4xOSIsImVtcGxveW1lbnRTdGF0dXMiOiJlbXBsb3llZCJ9fX0.Sazc8Ndhs-NKjxvtVMKeC9dxjEkI26fVsp2kFNWM-SYLtxMzKvl5ffeWd81ysHgPmBBSk2ar4dMqGgUsyM4gAQ'; + const signedNameAndDobVcJwt = + 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0I3o2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJOYW1lQW5kRG9iQ3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjliZjM2YzY5LTI0ODAtNDllZC1iMTYyLTRlZDEwOWE3MTc3NyIsImlzc3VlciI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0IiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDowNjowMVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlDS2o2M0FyZlBGcEpsb2lTd3gxQUhxVWtpWlNoSDZGdnZoSzRaTl9fZDFtQTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWNscFdXbTVJVkVrNWFEWkJUVmxVV0dwT01HcFhTVkYwTTI5ak4xTnJTeTF4Y2kxcVVuSTBUalEzUlNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpaVDFwRE5WSmlUMHQ1T0dadVVUWTJVWEZPUkc5aldFMXZPVXhUZEdNNVYyOWthMHd0ZFZCZlExQnZJaXdpZVNJNklsWnZZM0UxVERodFozQlhXVTFrYjFwS1JrWlJUa1ZDT0hsR0xXTndkRWQzZFdkcFRWVm5hR2t6Y21jaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xCTXpSMlMzb3llVmswZVV4dGRDMUdabkJuYWpWbGFFRm1ZWFI1YzFOa2MwNVNWbVpMYkhwUWRqTjVkeUo5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFF6ZGZjMXBzTW1wMVVXNUdhRVJIV1RSb2NFVTRiMlF4YVU5MWRuZG1PVFJ5TVVkbk9HMWFWbVJCVmxFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVKdU5sTnJiSEpWYzNKdVFuaFJPVXBqVXkxTlNVaGtWelYwTXpRM1MxWjNaMXBwVEZwMFQwcDRRVkYzSW4xOSIsIm5hbWUiOiJhbGljZSBib2IiLCJkYXRlT2ZCaXJ0aCI6IjEwLTAxLTE5OTAifX19.mNCDv_JntH-wZpYONKNL58UbOWaYXCYJO_HPI_WVlSgwzo6dhYmV_9qtpFKd_exFb-aaEYPeSE43twWlrJeSBg'; + const credentials = [signedEmploymentVcJwt, signedNameAndDobVcJwt]; + + + const presentationDefinition = { + id: 'presDefIdloanAppVerification123', + name: 'Loan Application Employment Verification', + purpose: 'To verify applicant’s employment, date of birth, and name', + input_descriptors: [ + // Employment Verification + { + id: 'employmentVerification', + purpose: 'Confirm current employment status', + constraints: { + fields: [ + { + path: ['$.credentialSubject.employmentStatus'], + filter: { + type: 'string', + pattern: 'employed', + }, + }, + ], + }, + }, + // Date of Birth Verification + { + id: 'dobVerification', + purpose: 'Confirm the applicant’s date of birth', + constraints: { + fields: [ + { + path: ['$.credentialSubject.dateOfBirth'], + filter: { + type: 'string', + format: 'date', + }, + }, + ], + }, + }, + // Name Verification + { + id: 'nameVerification', + purpose: 'Confirm the applicant’s legal name', + constraints: { + fields: [ + { + path: ['$.credentialSubject.name'], + filter: { + type: 'string' + } + } + ] + } + } + ] + }; + + test('getLoanAppPresentationDefinition returns a presentation definition', async () => { + // :snippet-start: getLoanAppPresentationDefinition + const presentationDefinition = { + id: 'presDefIdloanAppVerification123', + name: 'Loan Application Employment Verification', + purpose: 'To verify applicant’s employment, date of birth, and name', + input_descriptors: [ + // Employment Verification + { + id: 'employmentVerification', + purpose: 'Confirm current employment status', + constraints: { + fields: [ + { + path: ['$.credentialSubject.employmentStatus'], + filter: { + type: 'string', + pattern: 'employed', + }, + }, + ], + }, + }, + // Date of Birth Verification + { + id: 'dobVerification', + purpose: 'Confirm the applicant’s date of birth', + constraints: { + fields: [ + { + path: ['$.credentialSubject.dateOfBirth'], + filter: { + type: 'string', + format: 'date', + }, + }, + ], + }, + }, + // Name Verification + { + id: 'nameVerification', + purpose: 'Confirm the applicant’s legal name', + constraints: { + fields: [ + { + path: ['$.credentialSubject.name'], + filter: { + type: 'string' + } + } + ] + } + } + ] + }; + // :snippet-end: + expect(presentationDefinition).toBeDefined(); + expect(presentationDefinition).toHaveProperty('input_descriptors'); + expect(presentationDefinition.input_descriptors).toBeInstanceOf(Array); + expect(presentationDefinition.input_descriptors.length).toBe(3); + }); + test('selectCredentialsForPex selects VCs that match presentation defintion', async () => { + const allCredentials = credentials + // :snippet-start: selectCredentialsForPex + const selectedCredentials = PresentationExchange.selectCredentials({ + vcJwts: allCredentials, + presentationDefinition: presentationDefinition + }); + // :snippet-end: + expect(selectedCredentials).toBeDefined(); expect(selectedCredentials).toBeInstanceOf(Array); expect.soft(selectedCredentials.length).toBe(2); @@ -26,47 +150,64 @@ describe('Presentation Exchange Process', () => { expect.soft(selectedCredentials).toContain(signedNameAndDobVcJwt); }); - test('satisfiesPresentationDefinition() checks if VCs satisfy PD', async () => { - const satisfied = await pex_checkPresentationDefinitionSatisfaction( - credentials, - pd, - ); - expect(satisfied).toBe(true); + test('satisfiesPresentationDefinitionForPex checks if VCs satisfy PD', async () => { + const selectedCredentials = credentials + expect(() => { + // :snippet-start: satisfiesPresentationDefinitionForPex + try { + PresentationExchange.satisfiesPresentationDefinition({ + vcJwts: selectedCredentials, + presentationDefinition: presentationDefinition + }); + } catch (err) { + //Handle errors here + + } + // :snippet-end: + }).not.toThrow(); }); - test('createPresentationFromCredentials() creates a presentation result', async () => { - const presentationResult = await pex_createPresentationFromCredentials( - credentials, - pd, - ); + test('createPresentationFromCredentialsForPex creates a presentation result', async () => { + const selectedCredentials = credentials + // :snippet-start: createPresentationFromCredentialsForPex + const presentationResult = PresentationExchange.createPresentationFromCredentials({ + vcJwts: selectedCredentials, + presentationDefinition: presentationDefinition + }); + // :snippet-end: expect(presentationResult).toBeDefined(); - expect.soft(presentationResult).toHaveProperty('presentation'); - expect - .soft(presentationResult.presentation) - .toHaveProperty('presentation_submission'); - expect - .soft(presentationResult.presentation) - .toHaveProperty('verifiableCredential'); - expect - .soft(presentationResult.presentation.type) - .toContain('VerifiablePresentation'); - }); + expect(presentationResult).toHaveProperty('presentation'); + expect(presentationResult.presentation).toHaveProperty('presentation_submission'); + expect(presentationResult.presentation).toHaveProperty('verifiableCredential'); + expect(presentationResult.presentation.type).toContain('VerifiablePresentation'); + } + ); - test('validateSubmission() checks if the presentation submission is valid', async () => { - const pr = await pex_createPresentationFromCredentials(credentials, pd); - const validationResult = await pex_submissionCheck(pr); - expect(validationResult).toBeDefined(); - expect(validationResult).toBeInstanceOf(Array); - expect(validationResult.length).toBe(1); - expect.soft(validationResult[0]).toHaveProperty('tag', 'root'); - expect.soft(validationResult[0]).toHaveProperty('status', 'info'); - expect.soft(validationResult[0]).toHaveProperty('message', 'ok'); + test('validPresentationSubmissionForPex check if the presention submission is valid', async () => { + const presentationResult = await pex_createPresentationFromCredentials(credentials, pd); + // :snippet-start: validPresentationSubmissionForPex + const submissionCheck = PresentationExchange.validateSubmission({ + presentationSubmission: presentationResult.presentationSubmission + }); + // :snippet-end: + expect(submissionCheck.length).toBe(1); + expect.soft(submissionCheck[0]).toHaveProperty('tag', 'root'); + expect.soft(submissionCheck[0]).toHaveProperty('status', 'info'); + expect.soft(submissionCheck[0]).toHaveProperty('message', 'ok'); }); - test('presentationResult.presentation is a valid VP', async () => { - const pr = await pex_createPresentationFromCredentials(credentials, pd); - const presentation = await pex_getPresentationFromResult(pr); + test('validVerifiablePresentationForPex creates a valid VP', async () => { + const selectedCredentials = credentials + const presentationResult = PresentationExchange.createPresentationFromCredentials({ + vcJwts: selectedCredentials, + presentationDefinition: presentationDefinition + }); + // :snippet-start: validVerifiablePresentationForPex + const presentation = presentationResult.presentation; + // :snippet-end: + expect(presentation).toBeDefined(); expect(presentation.type).contains('VerifiablePresentation'); }); + }); diff --git a/site/testsuites/testsuite-kotlin/pom.xml b/site/testsuites/testsuite-kotlin/pom.xml index 68aa13ac1..826c6865f 100644 --- a/site/testsuites/testsuite-kotlin/pom.xml +++ b/site/testsuites/testsuite-kotlin/pom.xml @@ -25,14 +25,14 @@ These need to be uniformly updated as part of the single dependency script from Nick --> - 0.9.0-SNAPSHOT + 0.9.0-beta - 2.9.8 + 2.12.6 @@ -94,6 +94,16 @@ jackson-databind ${version.com.fasterxml.jackson.core} + + com.fasterxml.jackson.core + jackson-core + 2.12.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.12.1 + diff --git a/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/tbdex/wallet/PfiAllowListTest.kt b/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/tbdex/wallet/PfiAllowListTest.kt new file mode 100644 index 000000000..14d35b0a1 --- /dev/null +++ b/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/tbdex/wallet/PfiAllowListTest.kt @@ -0,0 +1,39 @@ + +package website.tbd.developer.site.docs.tbdex.wallet + +import foundation.identity.did.Service +import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.dids.methods.dht.CreateDidDhtOptions +import web5.sdk.dids.methods.dht.DidDht +import java.net.URI + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions.* + +/** + * Tests for Wallet AllowList guide + */ +class PfiAllowListTest { + + @Test + fun `check if DID has PFI service`() { + val serviceToAdd = Service.builder() + .id(URI("pfi")) + .type("PFI") + .serviceEndpoint("tbdex-pfi.tbddev.org") + .build() + + val options = CreateDidDhtOptions( + publish = false, + services = listOf(serviceToAdd), + ) + + val pfiDid = DidDht.create(InMemoryKeyManager(), options) + + // :snippet-start: isPFIKt + val isPFI = pfiDid.didDocument?.services?.any { it.type == "PFI" } ?: false + // :snippet-end: + + assertTrue(isPFI, "DID should have a PFI service") + } +} diff --git a/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/FanClubVcTest.kt b/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/FanClubVcTest.kt new file mode 100644 index 000000000..6394a461b --- /dev/null +++ b/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/FanClubVcTest.kt @@ -0,0 +1,198 @@ +package website.tbd.developer.site.docs.web5.build.verifiablecredentials; + +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions.* + +import web5.sdk.credentials.VerifiableCredential +import web5.sdk.credentials.PresentationExchange +import web5.sdk.credentials.model.* +import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.dids.methods.key.DidKey +import web5.sdk.credentials.VerifiablePresentation + +/** + * Tests backing the VC Fan Club Workflow + */ +internal class FanClubVcTest { + data class SwiftiesFanClub( + val level: String, + val legit: Boolean + ) + + val fanClubIssuerDid = DidKey.create(InMemoryKeyManager()) + val aliceDid = DidKey.create(InMemoryKeyManager()) + + val vc = VerifiableCredential.create( + type = "SwiftiesFanClub", + issuer = fanClubIssuerDid.uri, + subject = aliceDid.uri, + data = SwiftiesFanClub(level = "Stan", legit = true) + ) + + val signedVcJwt = vc.sign(fanClubIssuerDid) + + val presentationDefinition = PresentationDefinitionV2( + id = "presDefId123", + name = "Swifties Fan Club Presentation Definition", + purpose = "for proving membership in the fan club", + inputDescriptors = listOf( + InputDescriptorV2( + id = "legitness", + purpose = "are you legit or not?", + constraints = ConstraintsV2( + fields = listOf( + FieldV2( + path = listOf("$.vc.credentialSubject.legit") + ) + ) + ) + ) + ) + ) + + @Test + fun `createDidsKt imports dids package and creates dids`() { + // :snippet-start: createDidsKt + val fanClubIssuerDid = DidKey.create(InMemoryKeyManager()) + val aliceDid = DidKey.create(InMemoryKeyManager()) + // :snippet-end: + assertTrue(fanClubIssuerDid.uri.startsWith("did:key:"), "fanClubIssuerDid should start with 'did:key:'") + assertTrue(aliceDid.uri.startsWith("did:key:"), "aliceDid should start with 'did:key:'") + } + +@Test + fun `createClassCredentialKt imports credentials package and creates class for credential`() { + val createClassCredentialKt = + """ + // :snippet-start: createClassCredentialKt + import web5.sdk.credentials.VerifiableCredential + import web5.sdk.credentials.PresentationExchange + + data class SwiftiesFanClub( + // indicates the fan's dedication level + val level: String, + + // indicates if the fan is a genuine Swiftie + val legit: Boolean + ) + // :snippet-end: + """ + } + + @Test + fun `createFanClubVcKt creates a vc for fan club`() { + // :snippet-start: createFanClubVcKt + val vc = VerifiableCredential.create( + type = "SwiftiesFanClub", + issuer = fanClubIssuerDid.uri, + subject = aliceDid.uri, + data = SwiftiesFanClub(level = "Stan", legit = true) + ) + // :snippet-end: + + assertEquals("SwiftiesFanClub", vc.type) + assertEquals("Stan", vc.vcDataModel.credentialSubject.claims["level"]) + assertEquals(true, vc.vcDataModel.credentialSubject.claims["legit"]) + } + + @Test + fun `signFanClubVcKt signs a vc for fan club and returns jwt`() { + // :snippet-start: signFanClubVcKt + val signedVcJwt = vc.sign(fanClubIssuerDid) + // :snippet-end: + + assertTrue(signedVcJwt is String, "signedVcJwt should be a String") + } + + @Test + fun `createAndValidatePresentationKt creates and validates presentation definitionl`() { + // :snippet-start: createAndValidatePresentationKt + val presentationDefinition = PresentationDefinitionV2( + id = "presDefId123", + name = "Swifties Fan Club Presentation Definition", + purpose = "for proving membership in the fan club", + inputDescriptors = listOf( + InputDescriptorV2( + id = "legitness", + purpose = "are you legit or not?", + constraints = ConstraintsV2( + fields = listOf( + FieldV2( + path = listOf("$.vc.credentialSubject.legit") + ) + ) + ) + ) + ) + ) + + val definitionValidation = PresentationExchange.validateDefinition(presentationDefinition) + // :snippet-end: + assertDoesNotThrow {PresentationExchange.validateDefinition(presentationDefinition)} + } + + @Test + fun `satisfiesPresentationDefinitionFanClubVcKt checks if VC satisfies the presentation definition`() { + val outContent = ByteArrayOutputStream() + System.setOut(PrintStream(outContent)) + + // :snippet-start: satisfiesPresentationDefinitionFanClubVcKt + // Does VC Satisfy the Presentation Definition + try { + PresentationExchange.satisfiesPresentationDefinition(listOf(signedVcJwt), presentationDefinition) + println("VC Satisfies Presentation Definition!") + } catch (err: Exception) { + println("VC does not satisfy Presentation Definition: " + err.message) + } + // :snippet-end: + + assertTrue(outContent.toString().contains("VC Satisfies Presentation Definition!")) + System.setOut(System.out) + } + + @Test + fun `createPresentationFromCredentialsFanClubVcKt creates presentation from credentials and checks the presentation result`() { + // :snippet-start: createPresentationFromCredentialsFanClubVcKt + // Create Presentation Result that contains a Verifiable Presentation and Presentation Submission + val presentationResult = PresentationExchange.createPresentationFromCredentials(listOf(signedVcJwt), presentationDefinition) + + val vp = VerifiablePresentation.create( + vcJwts = listOf(signedVcJwt), + holder = aliceDid.uri, + additionalData = mapOf("presentation_submission" to presentationResult) + ) + println("Presentation Result and Verifiable Presentation" + vp) + // :snippet-end: + assertEquals("presDefId123", presentationResult.definitionId) + assertEquals("VerifiablePresentation", vp.vpDataModel.type) + } + + @Test + fun `verifyFanClubVcKt checks if VC verification is successful`() { + val outContent = ByteArrayOutputStream() + System.setOut(PrintStream(outContent)) + + // :snippet-start: verifyFanClubVcKt + try { + VerifiableCredential.verify(signedVcJwt) + println("VC Verification successful!") + } catch (err: Exception) { + println("VC Verification failed:" + err.message) + } + // :snippet-end: + assertTrue(outContent.toString().contains("VC Verification successful!")) + System.setOut(System.out) + } + + @Test + fun `parseFanClubJwtKt parses the signed VC JWT`() { + // :snippet-start: parseFanClubJwtKt + val parsedVc = VerifiableCredential.parseJwt(signedVcJwt) + // :snippet-end: + assertEquals("Stan", parsedVc.vcDataModel.credentialSubject.claims["level"]) + assertEquals(true, parsedVc.vcDataModel.credentialSubject.claims["legit"]) + } +} diff --git a/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/PresentationExchangeTest.kt b/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/PresentationExchangeTest.kt new file mode 100644 index 000000000..f75804024 --- /dev/null +++ b/site/testsuites/testsuite-kotlin/src/test/kotlin/docs/web5/build/verifiablecredentials/PresentationExchangeTest.kt @@ -0,0 +1,168 @@ +package website.tbd.developer.site.docs.web5.build.decentralizedidentifiers; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions.* +import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.credentials.PresentationExchange +import web5.sdk.credentials.VerifiableCredential +import web5.sdk.credentials.VerifiablePresentation +import web5.sdk.credentials.model.* + +/** + * Tests backing the Presentation Exchange Guide + */ +internal class PresentationExchangeTest { + val signedEmploymentVcJwt = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyI3o2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJFbXBsb3ltZW50Q3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjcyNDhiOTkyLTkwOTYtNDk2NS1hMGVjLTc3ZDhhODNhMWRmYiIsImlzc3VlciI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDoxMToyNVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlEMTR4UmY0cTJNWlh1ZWY2X2ZXYnBGbVlTUG94dGFxTkp1SmdEMG96Wl84UTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWVubGFNbVYzTlhKeVVXdFVjbUV3WlZsVk16WlBTblJzTURCbFJWZHhhalZhV0dkNmNEZFpSVTVKUVNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpQZDJZMFQyMUViamxKWm5SNFdYWnBkRTFHWm1jMVVXeDVMVVV6VWs1b1dsUkdPVlpFTWtnNVQzVjNJaXdpZVNJNkltUnZjVmxtV2s1c1NtRlRNVll4U201bU9HdEZObEF6VkRsd2QzaDNla3hFVTJWc1ZqTlRUa2s1U2xFaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xEWm05bVFUQkpVbU5uY2tWdVVHZHdRbU5RV1ZsV2VFWlliR0pTYjJRd2RVNWZRVkJwTkVrNUxVRmZRU0o5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFFtd3pWWG80VldGT2REZGxlREJKYjJJMFJFNXNhbFJGVmpaelQwTmtjbFJ3TWxvNE5FTkJPVFJPUWtFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVOWk9WRldZbWRKYkUxemRraEZYMVJtTld4a1MxQjBkR3d3WVV4blNrdHNSbmt6Vms0d2QzQTJhVFpSSW4xOSIsImVtcGxveW1lbnRTdGF0dXMiOiJlbXBsb3llZCJ9fX0.Sazc8Ndhs-NKjxvtVMKeC9dxjEkI26fVsp2kFNWM-SYLtxMzKvl5ffeWd81ysHgPmBBSk2ar4dMqGgUsyM4gAQ" + val signedNameAndDobVcJwt = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0I3o2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJOYW1lQW5kRG9iQ3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjliZjM2YzY5LTI0ODAtNDllZC1iMTYyLTRlZDEwOWE3MTc3NyIsImlzc3VlciI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0IiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDowNjowMVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlDS2o2M0FyZlBGcEpsb2lTd3gxQUhxVWtpWlNoSDZGdnZoSzRaTl9fZDFtQTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWNscFdXbTVJVkVrNWFEWkJUVmxVV0dwT01HcFhTVkYwTTI5ak4xTnJTeTF4Y2kxcVVuSTBUalEzUlNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpaVDFwRE5WSmlUMHQ1T0dadVVUWTJVWEZPUkc5aldFMXZPVXhUZEdNNVYyOWthMHd0ZFZCZlExQnZJaXdpZVNJNklsWnZZM0UxVERodFozQlhXVTFrYjFwS1JrWlJUa1ZDT0hsR0xXTndkRWQzZFdkcFRWVm5hR2t6Y21jaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xCTXpSMlMzb3llVmswZVV4dGRDMUdabkJuYWpWbGFFRm1ZWFI1YzFOa2MwNVNWbVpMYkhwUWRqTjVkeUo5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFF6ZGZjMXBzTW1wMVVXNUdhRVJIV1RSb2NFVTRiMlF4YVU5MWRuZG1PVFJ5TVVkbk9HMWFWbVJCVmxFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVKdU5sTnJiSEpWYzNKdVFuaFJPVXBqVXkxTlNVaGtWelYwTXpRM1MxWjNaMXBwVEZwMFQwcDRRVkYzSW4xOSIsIm5hbWUiOiJhbGljZSBib2IiLCJkYXRlT2ZCaXJ0aCI6IjEwLTAxLTE5OTAifX19.mNCDv_JntH-wZpYONKNL58UbOWaYXCYJO_HPI_WVlSgwzo6dhYmV_9qtpFKd_exFb-aaEYPeSE43twWlrJeSBg" + val allCredentials = listOf(signedEmploymentVcJwt, signedNameAndDobVcJwt) + + val presentationDefinition = PresentationDefinitionV2( + id = "presDefIdloanAppVerification123", + name = "Loan Application Employment Verification", + purpose = "To verify applicant’s employment, date of birth, and name", + inputDescriptors = listOf( + // Employment Verification + InputDescriptorV2( + id = "employmentVerification", + purpose = "Confirm current employment status", + constraints = ConstraintsV2( + fields = listOf(FieldV2(path = listOf("$.vc.credentialSubject.employmentStatus"))) + ) + ), + // Date of Birth Verification + InputDescriptorV2( + id = "dobVerification", + purpose = "Confirm the applicant’s date of birth", + constraints = ConstraintsV2( + fields = listOf(FieldV2(path = listOf("$.vc.credentialSubject.dateOfBirth"))) + ) + ), + // Name Verification + InputDescriptorV2( + id = "nameVerification", + purpose = "Confirm the applicant’s legal name", + constraints = ConstraintsV2( + fields = listOf(FieldV2(path = listOf("$.vc.credentialSubject.name"))) + ) + ) + ) +) + + @Test + fun `getLoanAppPresentationDefinitionKt `() { + // :snippet-start: getLoanAppPresentationDefinitionKt + val presentationDefinition = PresentationDefinitionV2( + id = "presDefIdloanAppVerification123", + name = "Loan Application Employment Verification", + purpose = "To verify applicant’s employment, date of birth, and name", + inputDescriptors = listOf( + // Employment Verification + InputDescriptorV2( + id = "employmentVerification", + purpose = "Confirm current employment status", + constraints = ConstraintsV2( + fields = listOf(FieldV2(path = listOf("$.vc.credentialSubject.employmentStatus"))) + ) + ), + // Date of Birth Verification + InputDescriptorV2( + id = "dobVerification", + purpose = "Confirm the applicant’s date of birth", + constraints = ConstraintsV2( + fields = listOf(FieldV2(path = listOf("$.vc.credentialSubject.dateOfBirth"))) + ) + ), + // Name Verification + InputDescriptorV2( + id = "nameVerification", + purpose = "Confirm the applicant’s legal name", + constraints = ConstraintsV2( + fields = listOf(FieldV2(path = listOf("$.vc.credentialSubject.name"))) + ) + ) + ) + ) + // :snippet-end: + + assertEquals("presDefIdloanAppVerification123", presentationDefinition.id) + assertEquals("Loan Application Employment Verification", presentationDefinition.name) + assertEquals("To verify applicant’s employment, date of birth, and name", presentationDefinition.purpose) + assertEquals(3, presentationDefinition.inputDescriptors.size) + } + + @Test + fun `selectCredentialsForPexKt selects VCs that match presentation defintion`() { + // :snippet-start: selectCredentialsForPexKt + val selectedCredentials = PresentationExchange.selectCredentials( + vcJwts = allCredentials, + presentationDefinition = presentationDefinition + ) + // :snippet-end: + assertNotNull(selectedCredentials, "Selected credentials should not be null") + assertTrue(selectedCredentials is List<*>, "Selected credentials should be a list") + assertEquals(2, selectedCredentials.size, "Selected credentials should contain 2 items") + assertTrue(signedEmploymentVcJwt in selectedCredentials, "Selected credentials should contain the employment VC JWT") + assertTrue(signedNameAndDobVcJwt in selectedCredentials, "Selected credentials should contain the name and DOB VC JWT") + } + + @Test + fun `satisfiesPresentationDefinitionForPexKt checks if VCs satisfy PD`() { + val selectedCredentials = allCredentials + assertDoesNotThrow { + // :snippet-start: satisfiesPresentationDefinitionForPexKt + try { + PresentationExchange.satisfiesPresentationDefinition( + vcJwts = selectedCredentials, + presentationDefinition = presentationDefinition + ) + } catch (e: Exception) { + // Handle errors here + } + // :snippet-end: + } + } + + @Test + fun `createPresentationFromCredentialsForPexKt creates a presentation result`() { + val holderDid = "did:key:zQ3shXrAnbgfytQYQjifUm2EcBBbRAeAeGfgC4TZrjw4X71iZ" + val selectedCredentials = allCredentials + // :snippet-start: createPresentationFromCredentialsForPexKt + val presentationResult = PresentationExchange.createPresentationFromCredentials( + vcJwts = selectedCredentials, + presentationDefinition = presentationDefinition + ) + + val verifiablePresentation = VerifiablePresentation.create( + vcJwts = selectedCredentials, + holder = holderDid, + additionalData = mapOf("presentation_submission" to presentationResult) + ) + // :snippet-end: + assertNotNull(verifiablePresentation, "Verifiable Presentation should not be null") + assertEquals(holderDid, verifiablePresentation.holder, "Holder DID should match") + } + + @Test + fun `validVerifiablePresentationForPexKt creates a valid VP`() { + val holderDid = "did:key:zQ3shXrAnbgfytQYQjifUm2EcBBbRAeAeGfgC4TZrjw4X71iZ" + val selectedCredentials = allCredentials + val presentationResult = PresentationExchange.createPresentationFromCredentials( + vcJwts = selectedCredentials, + presentationDefinition = presentationDefinition + ) + val verifiablePresentation = VerifiablePresentation.create( + vcJwts = selectedCredentials, + holder = holderDid, + additionalData = mapOf("presentation_submission" to presentationResult) + ) + // :snippet-start: validVerifiablePresentationForPexKt + val verifiablePresentationDataModelMap = verifiablePresentation.vpDataModel.toMap() + val mappedPresentationSubmission = verifiablePresentationDataModelMap["presentation_submission"] as? PresentationSubmission + // :snippet-end: + + assertNotNull(mappedPresentationSubmission, "Mapped Presentation Submission should not be null") + assertEquals(presentationResult.definitionId, mappedPresentationSubmission?.definitionId, "Definition ID should match") + } +} + +