Skip to content

Commit

Permalink
test(workflows): copy resident-card exchange test to workflows
Browse files Browse the repository at this point in the history
Signed-off-by: jrhender <[email protected]>
  • Loading branch information
jrhender committed Oct 30, 2024
1 parent e10bf3d commit c9de914
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2021 - 2023 Energy Web Foundation
* SPDX-License-Identifier: Apache-2.0
*/

import { plainToClass } from 'class-transformer';
import { WalletClient } from '../../../wallet-client';
import { VerifiablePresentationDto } from '../../../../src/vc-api/credentials/dtos/verifiable-presentation.dto';
import { CredentialDto } from '../../../../src/vc-api/credentials/dtos/credential.dto';
import { Presentation } from '../../../../src/vc-api/exchanges/types/presentation';
import { ExchangeDefinitionDto } from '../../../../src/vc-api/exchanges/dtos/exchange-definition.dto';
import { VpRequestInteractServiceType } from '../../../../src/vc-api/exchanges/types/vp-request-interact-service-type';
import { VpRequestQueryType } from '../../../../src/vc-api/exchanges/types/vp-request-query-type';
import { ProvePresentationOptionsDto } from '../../../../src/vc-api/credentials/dtos/prove-presentation-options.dto';

export class ResidentCardIssuance {
#exchangeId = 'permanent-resident-card-issuance';
#callbackUrl: string;
queryType = VpRequestQueryType.didAuth;

constructor(callbackUrl: string) {
this.#callbackUrl = callbackUrl;
}

getExchangeId(): string {
return this.#exchangeId;
}

getExchangeDefinition(): ExchangeDefinitionDto {
const exchangeDefinition: ExchangeDefinitionDto = {
exchangeId: this.#exchangeId,
query: [
{
type: this.queryType,
credentialQuery: []
}
],
interactServices: [
{
type: VpRequestInteractServiceType.mediatedPresentation
}
],
isOneTime: false,
callback: [
{
url: this.#callbackUrl
}
]
};
return plainToClass(ExchangeDefinitionDto, exchangeDefinition);
}

/**
*
* TODO: get and approve presentation review
* @param vp
* @param walletClient
* @returns
*/
async issueCredential(vp: VerifiablePresentationDto, walletClient: WalletClient) {
if (!vp.holder) {
return { errors: ['holder of vp not provided'] };
}
const issuingDID = await walletClient.createDID('key');
const credential = this.fillCredential(issuingDID.id, vp.holder);
const issueCredentialDto = {
options: {},
credential
};
const vc = await walletClient.issueVC(issueCredentialDto);
const presentation: Presentation = {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiablePresentation'],
verifiableCredential: [vc]
};
const verificationMethodURI = issuingDID?.verificationMethod[0]?.id;
if (!verificationMethodURI) {
return { errors: ['verification method for issuance not available'] };
}
const presentationOptions: ProvePresentationOptionsDto = {
verificationMethod: verificationMethodURI,
challenge: 'some-challenge'
};
const provePresentationDto = {
options: presentationOptions,
presentation
};
const returnVp = await walletClient.provePresentation(provePresentationDto);
return {
errors: [],
vp: returnVp
};
}

private fillCredential(issuingDID: string, holderDID: string): CredentialDto {
// This hard-coded example is from https://w3c-ccg.github.io/citizenship-vocab/#example
return {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://w3id.org/citizenship/v1'
// optional country-specific context can be added below
// e.g., https://uscis.gov/prc/v1
],
id: 'https://issuer.oidp.uscis.gov/credentials/83627465',
type: ['VerifiableCredential', 'PermanentResidentCard'],
issuer: issuingDID,
issuanceDate: '2019-12-03T12:19:52Z',
expirationDate: '2029-12-03T12:19:52Z',
credentialSubject: {
id: holderDID,
type: ['PermanentResident', 'Person'],
givenName: 'JOHN',
familyName: 'SMITH',
gender: 'Male',
image: 'data:image/png;base64,iVBORw0KGgo...kJggg==',
residentSince: '2015-01-01',
lprCategory: 'C09',
lprNumber: '999-999-999',
commuterClassification: 'C1',
birthCountry: 'Bahamas',
birthDate: '1958-07-17'
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2021 - 2023 Energy Web Foundation
* SPDX-License-Identifier: Apache-2.0
*/

import { plainToClass } from 'class-transformer';
import { ExchangeDefinitionDto } from '../../../../src/vc-api/exchanges/dtos/exchange-definition.dto';
import { VpRequestInteractServiceType } from '../../../../src/vc-api/exchanges/types/vp-request-interact-service-type';
import { VpRequestQueryType } from '../../../../src/vc-api/exchanges/types/vp-request-query-type';

export class ResidentCardPresentation {
#exchangeId = `b229a18f-db45-4b33-8d36-25d442467bab`;
#callbackUrl: string;
queryType = VpRequestQueryType.presentationDefinition;

constructor(callbackUrl: string) {
this.#callbackUrl = callbackUrl;
}

getExchangeId(): string {
return this.#exchangeId;
}

getExchangeDefinition(): ExchangeDefinitionDto {
const exchangeDefinition: ExchangeDefinitionDto = {
exchangeId: this.#exchangeId,
query: [
{
type: this.queryType,
credentialQuery: [
{
presentationDefinition: {
id: '286bc1e0-f1bd-488a-a873-8d71be3c690e',
input_descriptors: [
{
id: 'permanent_resident_card',
name: 'Permanent Resident Card',
purpose: 'We can only allow permanent residents into the application',
constraints: {
fields: [
{
path: ['$.type'],
filter: {
type: 'array',
contains: {
type: 'string',
const: 'PermanentResidentCard'
}
}
}
]
}
}
]
}
}
]
}
],
interactServices: [
{
type: VpRequestInteractServiceType.unmediatedPresentation
}
],
isOneTime: true,
callback: [
{
url: this.#callbackUrl
}
]
};
return plainToClass(ExchangeDefinitionDto, exchangeDefinition);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2021 - 2023 Energy Web Foundation
* SPDX-License-Identifier: Apache-2.0
*/

import { ProofPurpose } from '@sphereon/pex';
import * as request from 'supertest';
import * as nock from 'nock';
import { PresentationDto } from '../../../../src/vc-api/credentials/dtos/presentation.dto';
import {
ReviewResult,
SubmissionReviewDto
} from '../../../../src/vc-api/exchanges/dtos/submission-review.dto';
import { ResidentCardIssuance } from './resident-card-issuance.exchange';
import { ResidentCardPresentation } from './resident-card-presentation.exchange';
import { app, getContinuationEndpoint, vcApiBaseUrl, walletClient } from '../../../app.e2e-spec';
import { ProvePresentationOptionsDto } from 'src/vc-api/credentials/dtos/prove-presentation-options.dto';

const callbackUrlBase = 'http://example.com';
const callbackUrlPath = '/endpoint';
const callbackUrl = `${callbackUrlBase}${callbackUrlPath}`;

export const residentCardExchangeSuite = () => {
it('should support Resident Card issuance and presentation', async () => {
// As issuer, configure credential issuance exchange
// POST /exchanges
const exchange = new ResidentCardIssuance(callbackUrl);
const numHolderQueriesPriorToIssuance = 2;
const issuanceCallbackScope = nock(callbackUrlBase)
.post(callbackUrlPath)
.times(numHolderQueriesPriorToIssuance)
.reply(201);
await request(app.getHttpServer())
.post(`${vcApiBaseUrl}/exchanges`)
.send(exchange.getExchangeDefinition())
.expect(201);

// As holder, start issuance exchange
// POST /exchanges/{exchangeId}
const issuanceExchangeEndpoint = `${vcApiBaseUrl}/exchanges/${exchange.getExchangeId()}`;
const issuanceVpRequest = await walletClient.startExchange(issuanceExchangeEndpoint, exchange.queryType);
const issuanceExchangeContinuationEndpoint = getContinuationEndpoint(issuanceVpRequest);
expect(issuanceExchangeContinuationEndpoint).toContain(issuanceExchangeEndpoint);

// As holder, create new DID and presentation to authentication as this DID
const holderDIDDoc = await walletClient.createDID('key');
const holderVerificationMethod = holderDIDDoc.verificationMethod[0].id;
const options: ProvePresentationOptionsDto = {
verificationMethod: holderVerificationMethod,
proofPurpose: ProofPurpose.authentication,
challenge: issuanceVpRequest.challenge
};
const didAuthResponse = await request(app.getHttpServer())
.post(`${vcApiBaseUrl}/presentations/prove/authentication`)
.send({ did: holderDIDDoc.id, options })
.expect(201);
const didAuthVp = didAuthResponse.body;
expect(didAuthVp).toBeDefined();

// As holder, continue exchange by submitting did auth presentation
for (let i = 0; i < numHolderQueriesPriorToIssuance; i++) {
await walletClient.continueExchange(issuanceExchangeContinuationEndpoint, didAuthVp, true, true);
}
issuanceCallbackScope.done();

// As the issuer, get the transaction
// TODO TODO TODO!!! How does the issuer know the transactionId? -> Maybe can rely on notification
const urlComponents = issuanceExchangeContinuationEndpoint.split('/');
const transactionId = urlComponents.pop();
const transaction = await walletClient.getExchangeTransaction(exchange.getExchangeId(), transactionId);

// As the issuer, check the result of the transaction verification
expect(transaction.presentationSubmission.verificationResult.verified).toBeTruthy();
expect(transaction.presentationSubmission.verificationResult.errors).toHaveLength(0);

// As the issuer, create a presentation to provide the credential to the holder
const issueResult = await exchange.issueCredential(didAuthVp, walletClient);
const issuedVP = issueResult.vp; // VP used to wrapped issued credentials
const submissionReview: SubmissionReviewDto = {
result: ReviewResult.approved,
vp: issuedVP
};
await walletClient.addSubmissionReview(exchange.getExchangeId(), transactionId, submissionReview);

// As the holder, check for a reviewed submission
const secondContinuationResponse = await walletClient.continueExchange(
issuanceExchangeContinuationEndpoint,
didAuthVp,
false
);
const issuedVc = secondContinuationResponse.vp.verifiableCredential[0];
expect(issuedVc).toBeDefined();

// Configure presentation exchange
// POST /exchanges
const presentationExchange = new ResidentCardPresentation(callbackUrl);
const presentationCallbackScope = nock(callbackUrlBase).post(callbackUrlPath).reply(201);
const exchangeDef = presentationExchange.getExchangeDefinition();
await request(app.getHttpServer()).post(`${vcApiBaseUrl}/exchanges`).send(exchangeDef).expect(201);

// Start presentation exchange
// POST /exchanges/{exchangeId}
const exchangeEndpoint = `${vcApiBaseUrl}/exchanges/${presentationExchange.getExchangeId()}`;
const presentationVpRequest = await walletClient.startExchange(
exchangeEndpoint,
presentationExchange.queryType
);
const presentationExchangeContinuationEndpoint = getContinuationEndpoint(presentationVpRequest);
expect(presentationExchangeContinuationEndpoint).toContain(exchangeEndpoint);

// Holder should parse VP Request for correct credentials...
// Assume that holder figures out which VC they need and can prep presentation
const presentation: PresentationDto = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://www.w3.org/2018/credentials/examples/v1'
],
type: ['VerifiablePresentation'],
verifiableCredential: [issuedVc],
holder: holderDIDDoc.id
};
const presentationOptions: ProvePresentationOptionsDto = {
verificationMethod: holderVerificationMethod,
proofPurpose: ProofPurpose.authentication,
created: '2021-11-16T14:52:19.514Z',
challenge: presentationVpRequest.challenge
};
const vp = await walletClient.provePresentation({ presentation, options: presentationOptions });

// Holder submits presentation
await walletClient.continueExchange(presentationExchangeContinuationEndpoint, vp, false);
presentationCallbackScope.done();
});
};

0 comments on commit c9de914

Please sign in to comment.