From 37cac7aa2a052d3ace6d42b351819b6458796522 Mon Sep 17 00:00:00 2001 From: Sudo Platform Engineering Date: Mon, 22 Nov 2021 04:02:26 +0000 Subject: [PATCH] Release 0.4.0 --- README.md | 12 +- e2e/browser-server.sh | 115 +- e2e/commonHelpers.ts | 143 +- e2e/config.ts | 20 +- e2e/credentialDefinition.test.ts | 20 +- e2e/credentialDefinitionHelpers.ts | 94 +- e2e/credentialIssuance.test.ts | 82 +- e2e/credentialIssuanceHelpers.ts | 120 +- e2e/credentialRevocation.test.ts | 252 + e2e/docker/docker-compose-test.yml | 37 + e2e/docker/nginx-default.conf | 48 + e2e/global-setup.ts | 9 - e2e/global-teardown.ts | 10 - e2e/proofPresentation.test.ts | 32 +- e2e/proofPresentationHelpers.ts | 148 +- e2e/schemaDefinition.test.ts | 8 +- e2e/service.ts | 22 - e2e/setup-tests.ts | 2 +- jest.e2e.config.js | 22 +- package.json | 30 +- src/components/Form/DIDCommSelectionItem.tsx | 2 +- .../IndyCredentialDefinitionIdEntryItem.tsx | 53 + src/components/Form/IndySchemaIdEntryItem.tsx | 3 +- src/components/Proofs/CompletedProofsCard.tsx | 31 +- src/components/Proofs/CompletedProofsList.tsx | 58 +- src/models/ACAPy/Connections.ts | 13 +- src/models/ACAPy/CredentialDefinitions.ts | 12 +- src/models/ACAPy/CredentialIssuance.ts | 73 +- src/models/ACAPy/DecentralizedIdentifiers.ts | 8 +- src/models/ACAPy/ProofPresentation.ts | 15 +- src/models/ACAPy/SchemaDefinitions.ts | 8 +- .../ACAPy/TransactionAuthorAgreement.ts | 4 +- .../ConnectionsCard/ConnectionsCard.tsx | 13 - .../ConnectionsCard/ConnectionsList.tsx | 2 + .../CreateCredentialDefinitionForm.tsx | 97 +- .../CredentialDefinitionsList.tsx | 38 +- .../CredentialDefinitionsCard.spec.tsx.snap | 31 + .../CreateSchemaDefinitionForm.tsx | 62 +- .../SchemaDefinitionsCard.spec.tsx | 6 +- .../SchemaDefinitionsList.tsx | 2 +- .../ActiveCredentialRequestsCard.tsx | 25 +- .../ActiveCredentialRequestsList.tsx | 12 +- .../IssuedCredentialActionMenu.tsx | 71 + .../IssuedCredentialsCard.tsx | 58 +- .../IssuedCredentialsList.tsx | 105 +- .../IssuerCredentialStatusIcon.tsx | 63 + .../ActiveProofRequestsCard.tsx | 19 +- .../ActiveProofRequestsList.tsx | 19 +- .../RequestProofAttributeList.tsx | 53 +- .../RequestProofForm.tsx | 131 +- .../CredentialRequestsCard.tsx | 13 - .../CredentialRequestsList.tsx | 15 +- .../ProposeCredentialForm.tsx | 82 +- .../HolderCredentialStatusIcon.tsx | 54 + .../OwnedCredentialsCard.tsx | 20 +- .../OwnedCredentialsList.tsx | 11 + .../ActiveProofPresentationsCard.tsx | 29 +- .../ActiveProofPresentationsList.tsx | 19 +- src/utils/errorlog.ts | 4 +- ...atform-labs-sudo-di-cloud-agent-v0.4.0.tgz | Bin 918161 -> 0 bytes utils/update-acapy-address.sh | 12 +- yarn.lock | 4113 ++++++++--------- 62 files changed, 3869 insertions(+), 2816 deletions(-) create mode 100644 e2e/credentialRevocation.test.ts create mode 100644 e2e/docker/docker-compose-test.yml create mode 100644 e2e/docker/nginx-default.conf delete mode 100644 e2e/global-setup.ts delete mode 100644 e2e/global-teardown.ts delete mode 100644 e2e/service.ts create mode 100644 src/components/Form/IndyCredentialDefinitionIdEntryItem.tsx create mode 100644 src/pages/CredentialIssuer/CredentialIssuance/IssuedCredentialsCard/IssuedCredentialActionMenu.tsx create mode 100644 src/pages/CredentialIssuer/CredentialIssuance/IssuedCredentialsCard/IssuerCredentialStatusIcon.tsx create mode 100644 src/pages/HolderWallet/Credentials/OwnedCredentialsCard/HolderCredentialStatusIcon.tsx delete mode 100644 sudoplatform-labs-sudo-di-cloud-agent-v0.4.0.tgz diff --git a/README.md b/README.md index ba9b604..8468ad1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Sudo Decentralized Identity Cloud Agent Admin Console The Sudo Decentralized Cloud Agent Admin Console implements a Sample Web UI to control the process of -creating, issuing and holding Verifiable Credentials. This Web UI utilises the +creating, issuing, holding, verifying and revoking Verifiable Credentials. This Web UI utilises the [Sudo Decentralized Identity Cloud Agent SDK](https://sudoplatform-labs.github.io/sudo-di-cloud-agent-js/) to create a local, standalone, Decentralized Identity development environment. @@ -11,7 +11,7 @@ _NOTE:_ Currently only MacOS is supported as a development environment and Chrom | Technology | Supported version | | -------------- | ----------------- | -| docker desktop | 3.1.0 | +| docker desktop | 4.1.1 | | yarn | 1.22.10 | | node | 12.6.2 | @@ -25,17 +25,17 @@ Before begining these instructions you must have [yarn](https://yarnpkg.com) and This will initialise the `node_modules` directory with project dependencies, including the [Sudo Decentralized Identity Cloud Agent SDK](https://sudoplatform-labs.github.io/sudo-di-cloud-agent-js/). 4. Edit `public/acapy.json` and add element `"endorserSeed": “”,` 5. Start the local development environment using the installed - [Sudo Decentralized Identity Cloud Agent SDK](https://sudoplatform-labs.github.io/sudo-di-cloud-agent-js/), command `yarn di-env start -c $PWD/public/acapy.json`. + [Sudo Decentralized Identity Cloud Agent SDK](https://sudoplatform-labs.github.io/sudo-di-cloud-agent-js/), command `yarn di-env up -c $PWD/public/acapy.json`. This will download and start local docker instances of both a [VON Network](https://github.com/bcgov/von-network) Indy Ledger and the Sudo Cloud Agency Service, which is derived from the [hyperledger Aries Cloud Agent Python](https://github.com/hyperledger/aries-cloudagent-python). See `yarn di-env -h` for other command options. 6. Run `yarn start` to begin serving the Web UI. 7. Browse to the web page url at `localhost:3000` 8. Stop the local development environment when finished using `yarn di-env down`. - **NOTE:** This will destroy all credentials, schemas and DIDs created. + **NOTE:** This will destroy all credentials, schemas and DIDs created. Use `yarn di-env stop` if you want to pause the containers whilst maintaining state and `yarn di-env start` to resume. **_Troubleshooting_** -It is possible that the `yarn di-env down/start` commands could fail. Instances of this occuring usually relate to a change of the `node_modules/@sudoplatform-labs/sudo-di-cloud-agent` +It is possible that the `yarn di-env up/start` commands could fail. Instances of this occuring usually relate to a change of the `node_modules/@sudoplatform-labs/sudo-di-cloud-agent` package whilst the environment is running (e.g. performing a `yarn upgrade`). The simplest way to recover from such errors is to manualy terminate the docker containers related to the VON-network and sudo-di-cloud-agent (i.e. via the docker desktop user interface). ## Using a Public Ledger and Public Cloud Agent Endpoint @@ -43,7 +43,7 @@ package whilst the environment is running (e.g. performing a `yarn upgrade`). Th **IMPORTANT** : When using a public ledger, information written is persistent and immutable. Personally Identifiable Information (PII) **MUST NOT** be written and is a major reason why the Transaction Authorisation Agreement must be signed in the acceptance/setup process. It is recommended that as much development activity as possible is performed with the local VON Ledger before using a Public ledger. -The Web UI can support using a public ledger such as [Sovrin BuilderNet or StagingNet](https://sovrin.org/overview). +The Web UI can support using a public ledger such as [Sovrin BuilderNet/StagingNet](https://sovrin.org/overview) or [Indicio TestNet/DemoNet](https://indicio.tech/indicio-testnet/). When using these ledgers the UI will automatically display the Transaction Authorisation Agreement and require acceptance before any ledger write operation (e.g Schema Creation, Credential Definition, DID Writes). For details on starting the Development Environment with public ledgers and/or creating a public Cloud Agent endpoint, refer to the [Sudo Decentralized Identity Cloud Agent SDK](https://www.npmjs.com/package/@sudoplatform-labs/sudo-di-cloud-agent) documentation. diff --git a/e2e/browser-server.sh b/e2e/browser-server.sh index e0ab872..212612c 100755 --- a/e2e/browser-server.sh +++ b/e2e/browser-server.sh @@ -1,7 +1,7 @@ #!/bin/bash # -# This script starts/stops a test bowser server in a docker container -# so that e2e tests can be isolated from browser versions +# This script brings up/shutsdown a test bowser server and webserver in +# docker containers so that e2e tests can be isolated from browser versions # # Usage: function usage() { @@ -10,13 +10,14 @@ function usage() { Commands: - start - Create a local browser server docker container + up - Create local browser server and webserver docker containers start options : -i : docker image to use - -p : port to map to the browser control port inside the container + -p : port to map to the browser server control port inside the container - stop - Stop running instance of browser server docker container + down - Bring down running instance of the browser server an webserver + docker environment. EOF exit 1 @@ -28,8 +29,8 @@ exit 1 # Check Programs needed are installed ########################################################################################## -type docker >/dev/null 2>&1 || { - echo >&2 "docker is required but is not installed. Aborting." +type docker-compose >/dev/null 2>&1 || { + echo >&2 "docker-compose is required but is not installed. Aborting." exit 1 } @@ -43,9 +44,6 @@ fi # Make sure everything is done starting in our commands home directory cd ${REAL_PWD} ROOT_DIR="${REAL_PWD}/.." -DEVEL_DIR="${ROOT_DIR}/devel" - -BROWSER_DOCKER_NAME_FILE="${DEVEL_DIR}/browser_runner.json" # Print an indication of script reaching a processing # milestone in a noticable way @@ -70,73 +68,36 @@ function runEval() { return $returnValue } -# Pull the value for a specific field out of a simple JSON -# format object and echo it. -# $1 : The field name -# $2 : The JSON file name -function getJSONFieldValue() { - returnValue=`awk -F'"' '/'${1}'/ { print $4 }' ${2}` - echo $returnValue -} -# $1: The name of a variable to return the IP address to -function getHostIP() { - local hostOS="$(uname -s)" - local hostIP +# Start docker browser server and webserver containers +# $1: The browser server docker image to use +# $2: The host port number to map the browser server command port onto +function upBrowserEnv() { + export BROWSER_SERVER_DOCKER_IMAGE=$(cut -d':' -f1 <<<${1}) + export BROWSER_SERVER_DOCKER_TAG=$(cut -d':' -f2 <<<${1}) + export BROWSER_SERVER_CONTROL_EXTERNAL_PORT=${2} + export FRONTEND_NETWORK="von_von"; + export FRONTEND_EXTERNAL="true" - if [ ${hostOS} = "Darwin" ]; then - # Look at wired interface first - hostIP=$(ipconfig getifaddr en1) - if [[ $? != 0 ]]; then - hostIP=$(ipconfig getifaddr en0) - fi - else - hostIP=$(hostname -i | awk '{print $1}') - fi - - local result=${1} - if [[ "${result}" ]]; then - eval ${result}="'${hostIP}'" - fi -} - - -# Start a docker standalone browser container which has -# the host machines address injected into its DNS knowledge -# $1: The browser docker image to use -# $2: The host port number to map the browser command port onto -# $3: The name of a variable to return the container ID to -function startBrowserServer() { - acapyContainer=$(docker ps | grep sudo-di-cloud-agent | awk '{print $1}') - getHostIP DOCKER_HOST_IP - randName=$(cat /dev/urandom | env LC_CTYPE=ALL tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1) - browserCmd="docker run -d -e SCREEN_WIDTH=1600 -e SCREEN_HEIGHT=1200 --name browser-server_${randName} --rm --link=${acapyContainer} --add-host react.webserver:${DOCKER_HOST_IP} \ - -p ${2}:4444 -p 5900:5900 -v /dev/shm:/dev/shm ${1}" + upCmd="docker-compose -f ${ROOT_DIR}/e2e/docker/docker-compose-test.yml -p browser-environment up -d" - printMilestone "Starting browser server docker image with command: \n \ - \t ${browserCmd}" + printMilestone "Starting browser server and webserver docker images with command: \n \ + \t ${upCmd}" - containerId=$(${browserCmd}) + ${upCmd} local returnStatus=$? if [[ ${returnStatus} != 0 ]]; then - echo "**** FAIL - Browser Server failed to start, exiting. ****" + echo "**** FAIL - Browser Server and Webserver failed to start, exiting. ****" exit 1 fi - local result=${3} - if [[ "${result}" ]]; then - eval ${result}="'${containerId}'" - fi } -# Stop a running browser server docker container -# $1: The docker container identifier -function stopBrowserServer() { - local containerId="${1}" - stopCmd="docker stop ${containerId}" +# Stop a running browser server and webserver docker containers +function downBrowserEnv() { + downCmd="docker-compose -f ${ROOT_DIR}/e2e/docker/docker-compose-test.yml -p browser-environment down" - printMilestone "Stopping Browser Server docker container id : \n \ - \t ${containerId}" - ${stopCmd} + printMilestone "Stopping Browser Server and Webserver docker containers" + ${downCmd} } @@ -144,12 +105,12 @@ function stopBrowserServer() { # MAIN LINE ########################################################################################## -# Support start, and stop commands +# Support up, and down commands subCommand=$1 shift || usage; case "${subCommand}" in - start) + up) # start comes with several options on how to construct the environment while getopts ':i:p:' option; do case ${option} in @@ -161,23 +122,11 @@ case "${subCommand}" in # Remove processed options shift $((OPTIND -1)) - if [ ! -d "${DEVEL_DIR}" ]; then - runEval "mkdir -p ${DEVEL_DIR}" - fi - startBrowserServer ${imageOption} ${hostPortOption} containerId - # Save the browser server docker container name so that it can be killed on stop - # commands and not potentially destroy another instance we didn't - # start. - cat < ${BROWSER_DOCKER_NAME_FILE} - { - "browserDockerContainer": "${containerId}" - } -EOF + upBrowserEnv ${imageOption} ${hostPortOption} ;; - stop) - containerId=$(getJSONFieldValue "browserDockerContainer" ${BROWSER_DOCKER_NAME_FILE}) - stopBrowserServer ${containerId} + down) + downBrowserEnv ;; *) usage; esac diff --git a/e2e/commonHelpers.ts b/e2e/commonHelpers.ts index 02d35c3..673db3f 100644 --- a/e2e/commonHelpers.ts +++ b/e2e/commonHelpers.ts @@ -5,7 +5,7 @@ export const commonWaitDefault = 20000; /** * Utility function to wait for a element to appear and be - * visible. Returns to element if found. + * visible. Returns the element if found. * * @param {?number} wait an optional amount of time to use when waiting for * elements to resolve when locating them. @@ -28,6 +28,26 @@ export async function e2eWaitElementVisible( } } +export async function e2eScrollToElement( + locator: Locator, + wait: number = commonWaitDefault, +): Promise { + try { + const element = await driver.wait(until.elementLocated(locator), wait); + + // Move will scroll down and bring the element into view + await driver + .actions({ bridge: true }) + .pause(200) + .move({ origin: element }) + .pause(200) + .perform(); + return element; + } catch (e) { + throw `Failed to scroll to element ${locator}: ${e}`; + } +} + /** * Utility function to test navigation to a specified card * and verify presence. @@ -324,6 +344,7 @@ export async function e2eExecuteTableRowDropdownAction( .move({ origin: element }) .pause(200) .click() + .pause(200) .perform(); } @@ -483,3 +504,123 @@ export async function e2eAcceptTAAForm( ) ).click(); } + +/** + * Utility function to input a time into an antd DatePicker + * Expects the DatePicker to be currently displaying on entry + * + * @param {!string} locatorPrefix a string that can be used at the front of an XPATH + * to locate the specific DatePicker element of interest + * @param {!Date} date a Date object representing the entryDate Date/Time to + * select in the RangePicker + * @param {?number} wait an optional amount of time to use when waiting for + * elements to resolve when locating them. + */ +export async function e2eEnterDatePickerDetails( + locatorPrefix: string, + entryDate: Date, + wait: number = commonWaitDefault, +): Promise { + const monthNumberFromString = (month: string): number => { + return new Date(Date.parse(month + ' 1, 2000')).getMonth() + 1; + }; + + // Split the start date up into YY,MM,DD, hh, mm, ss components + // to use in navigating the picker components + const YY = String(entryDate.getUTCFullYear()).padStart(4, '0'); + const MM = String(entryDate.getUTCMonth() + 1).padStart(2, '0'); + const DD = String(entryDate.getUTCDate()).padStart(2, '0'); + const hh = String(entryDate.getUTCHours()).padStart(2, '0'); + const mm = String(entryDate.getUTCMinutes()).padStart(2, '0'); + const ss = String(entryDate.getUTCSeconds()).padStart(2, '0'); + + // Calculate the number of moves to get to the requested + // year and month + const yearMoves = + parseInt(YY) - + parseInt( + await ( + await driver.wait( + until.elementLocated( + By.xpath( + `${locatorPrefix}//div[@class='ant-picker-date-panel']/div/div/button[@class='ant-picker-year-btn']`, + ), + ), + wait, + ) + ).getText(), + ); + + const monthMoves = + parseInt(MM) - + monthNumberFromString( + await ( + await driver.wait( + until.elementLocated( + By.xpath( + `${locatorPrefix}//div[@class='ant-picker-date-panel']/div/div/button[@class='ant-picker-month-btn']`, + ), + ), + wait, + ) + ).getText(), + ); + + // Action the moves to correct year then correct month + const yearMoveButton = + yearMoves > 0 + ? `${locatorPrefix}//div[@class='ant-picker-date-panel']/div/button[@class='ant-picker-header-super-next-btn']` + : `${locatorPrefix}//div[@class='ant-picker-date-panel']/div/button[@class='ant-picker-header-super-prev-btn']`; + + for (let i = Math.abs(yearMoves); i > 0; i--) { + await ( + await driver.wait(until.elementLocated(By.xpath(yearMoveButton)), wait) + ).click(); + } + + const monthMoveButton = + monthMoves > 0 + ? `${locatorPrefix}//div[@class='ant-picker-date-panel']/div/button[@class='ant-picker-header-next-btn']` + : `${locatorPrefix}//div[@class='ant-picker-date-panel']/div/button[@class='ant-picker-header-prev-btn']`; + + for (let i = Math.abs(monthMoves); i > 0; i--) { + await ( + await driver.wait(until.elementLocated(By.xpath(monthMoveButton)), wait) + ).click(); + } + + // Select Day, Hour, Minute and Seconds from selection Arrays + const dayDate = `${YY}-${MM}-${DD}`; + await ( + await e2eWaitElementVisible( + By.xpath( + `${locatorPrefix}//div[@class='ant-picker-date-panel']/div[@class='ant-picker-body']//td[@title='${dayDate}']`, + ), + wait, + ) + ).click(); + + await ( + await e2eScrollToElement( + By.xpath( + `${locatorPrefix}//div[@class='ant-picker-time-panel']/div[@class='ant-picker-content']/ul[1]/li/div[contains(.,'${hh}')]`, + ), + ) + ).click(); + + await ( + await e2eScrollToElement( + By.xpath( + `${locatorPrefix}//div[@class='ant-picker-time-panel']/div[@class='ant-picker-content']/ul[2]/li/div[contains(.,'${mm}')]`, + ), + ) + ).click(); + + await ( + await e2eScrollToElement( + By.xpath( + `${locatorPrefix}//div[@class='ant-picker-time-panel']/div[@class='ant-picker-content']/ul[3]/li/div[contains(.,'${ss}')]`, + ), + ) + ).click(); +} diff --git a/e2e/config.ts b/e2e/config.ts index 68ad025..d0e6147 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -10,13 +10,17 @@ import { join } from 'path'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; export const env = { - // The host has to be injected into the the docker browser server - // using `--add-host react.webserver:` with the IP - // address of the docker host - BASE_URL: 'http://react.webserver:3000', - BROWSER: 'chrome', - BROWSER_SERVER: 'http://localhost:4445/wd/hub', - HEADLESS: false, + // The browser server accesses the admin console on the + // internal docker-compose frontend network since it is running + // inside the docker network context. So we can use a docker-compose + // assigned name and the internal port value. + BASE_URL: process.env['TEST_BASE_URL'] ?? 'http://admin-webserver:80', + BROWSER: process.env['BROWSER_TYPE'] ?? 'chrome', + // The browser server is accessed from scripts running + // outside the docker network context on a mapped port. + BROWSER_SERVER: + process.env['BROWSER_SERVER_URL'] ?? 'http://localhost:4445/wd/hub', + HEADLESS: process.env['HEADLESS_BROWSER'] ?? 'false', }; const driversToCleanUp: WebDriver[] = []; @@ -64,7 +68,7 @@ function setCommonOptions( ): T { options.windowSize({ width: 1600, height: 1200 }); - if (env.HEADLESS) { + if (env.HEADLESS == 'true') { options.headless(); } diff --git a/e2e/credentialDefinition.test.ts b/e2e/credentialDefinition.test.ts index 4beec37..9d924ed 100644 --- a/e2e/credentialDefinition.test.ts +++ b/e2e/credentialDefinition.test.ts @@ -32,17 +32,33 @@ describe('Credential Definition', function () { it('CD-0101 Create valid basic credential definition', async function () { const schemaId = await e2eCreateSchemaDefinition( - 'CD-0101 Test Schema', + 'CD-0101_Test_Schema', '1.0', ['firstName', 'lastName', 'someCredentialValue'], ); await e2eCreateCredentialDefinition( - 'CD-0101 Test Credential Definition', + 'CD-0101_Test_Credential_Definition', schemaId, ); }); + it('CD-0102 Create valid revocable credential definition', async function () { + const schemaId = await e2eCreateSchemaDefinition( + 'CD-0102_Test_Schema', + '1.0', + ['firstName', 'lastName', 'someCredentialValue'], + ); + + await e2eCreateCredentialDefinition( + 'CD-0102_Test_Credential_Definition', + schemaId, + true, + '10000', + '1000', + ); + }); + it('CD-0201 Attempt invalid create credential definition without name or schema', async function () { await e2eNavigateToCredentialDefinitionsCard(); diff --git a/e2e/credentialDefinitionHelpers.ts b/e2e/credentialDefinitionHelpers.ts index 1b1a6a2..8a00f35 100644 --- a/e2e/credentialDefinitionHelpers.ts +++ b/e2e/credentialDefinitionHelpers.ts @@ -1,4 +1,4 @@ -import { By, until } from 'selenium-webdriver'; +import { By, Key } from 'selenium-webdriver'; import { e2eAcceptTAAForm, e2eCheckMessageDisplays, @@ -21,6 +21,31 @@ export async function e2eNavigateToCredentialDefinitionsCard(): Promise { ); } +export async function e2eGetCredentialDefinitionId( + name: string, +): Promise { + await e2eNavigateToCredentialDefinitionsCard(); + + // Obtain the credential identifier to return + await ( + await e2eWaitElementVisible( + By.xpath(`//td[contains(.,'${name}')]/../td[1]`), + cdWaitDefault, + ) + ).click(); + + const credentialDefinitionId = await ( + await e2eWaitElementVisible( + By.xpath( + "//h3[contains(.,'Credential Definition Identifier')]/following-sibling::p", + ), + cdWaitDefault, + ) + ).getText(); + + return credentialDefinitionId; +} + /** * Utility function to create a credential definition * given a name and schema identifier @@ -29,11 +54,19 @@ export async function e2eNavigateToCredentialDefinitionsCard(): Promise { * credential definition. * @param {!string} schemId the full identifier of the schema to associate * with this credential definition. + * @param {boolean} revocable whether credentials issued from this definition + * can be revoked + * @param {string} old_registry_size the size field value expected on entry + * @param {string} new_registry_size the number of revocable credentials to allocate + * in each revocable_registry. */ export async function e2eCreateCredentialDefinition( name: string, schemaId: string, -): Promise { + revocable?: boolean, + old_registry_size?: string, + new_registry_size?: string, +): Promise { await e2eNavigateToCredentialDefinitionsCard(); await ( @@ -54,6 +87,39 @@ export async function e2eCreateCredentialDefinition( // Use the SchemaId from our created schema await (await driver.findElement(By.id('schemaId'))).sendKeys(schemaId); + // If revocable is indicated toggle switch + if (revocable) { + await (await driver.findElement(By.id('revocable'))).click(); + } + // If registry size is indicated, set that too + if (old_registry_size) { + expect(await driver.findElement(By.id('size')).getAttribute('value')).toBe( + old_registry_size, + ); + } + + if (new_registry_size) { + const element = await driver.findElement(By.id('size')); + + while ((await element.getAttribute('value')) !== '') { + await element.sendKeys(Key.BACK_SPACE); + } + + await driver + .actions({ bridge: true }) + .pause(200) + .move({ origin: element }) + .pause(200) + .click() + .pause(200) + .sendKeys(new_registry_size) + .perform(); + + expect(await driver.findElement(By.id('size')).getAttribute('value')).toBe( + new_registry_size, + ); + } + await ( await e2eWaitElementVisible( By.css('#CreateCredentialDefinitionForm__submit-btn > span'), @@ -68,9 +134,23 @@ export async function e2eCreateCredentialDefinition( cdWaitDefault, ); - // Verify the credential has been put into the table - await driver.wait( - until.elementLocated(By.xpath(`//td[contains(.,'${name}')]`)), - cdWaitDefault, - ); + // Obtain the credential definition identifier to return, which also ensures it was + // created and displayed. + await ( + await e2eWaitElementVisible( + By.xpath(`//td[contains(.,'${name}')]/../td[1]`), + cdWaitDefault, + ) + ).click(); + + const credentialDefinitionId = await ( + await e2eWaitElementVisible( + By.xpath( + "//h3[contains(.,'Credential Definition Identifier')]/following-sibling::p", + ), + cdWaitDefault, + ) + ).getText(); + + return credentialDefinitionId; } diff --git a/e2e/credentialIssuance.test.ts b/e2e/credentialIssuance.test.ts index 9ad6f4c..0f95e00 100644 --- a/e2e/credentialIssuance.test.ts +++ b/e2e/credentialIssuance.test.ts @@ -9,7 +9,10 @@ import { e2eWaitElementVisible, } from './commonHelpers'; import { e2eAcceptInvitation, e2eCreateInvitation } from './connectionHelpers'; -import { e2eCreateCredentialDefinition } from './credentialDefinitionHelpers'; +import { + e2eCreateCredentialDefinition, + e2eGetCredentialDefinitionId, +} from './credentialDefinitionHelpers'; import { e2eNavigateToRequestedCredentialsCard, e2eNavigateToOwnedCredentialsCard, @@ -19,10 +22,7 @@ import { ciWaitDefault, e2eSendCredentialProposal, } from './credentialIssuanceHelpers'; -import { - e2eCreateSchemaDefinition, - e2eGetSchemaId, -} from './schemaDefinitionHelpers'; +import { e2eCreateSchemaDefinition } from './schemaDefinitionHelpers'; import { driver } from './setup-tests'; describe('Credential Issuance', function () { @@ -78,29 +78,36 @@ describe('Credential Issuance', function () { await e2eAcceptInvitation('CI Issuer', invitationText); const schemaId = await e2eCreateSchemaDefinition( - 'CI-0101 Test Schema', + 'CI-0101_Test_Schema', '1.0', ['firstName', 'lastName', 'someCredentialValue'], ); - await e2eCreateCredentialDefinition( - 'CI-0101 Test Credential Definition', + const credentialDefinitionId = await e2eCreateCredentialDefinition( + 'CI-0101_Test_Credential_Definition', schemaId, ); - await e2eObtainCredential(schemaId, 'CI-0101 Test Message', 'CI Issuer', [ - { name: 'firstName', value: 'CI-0101 firstName' }, - { name: 'lastName', value: 'CI-0101 lastName' }, - { name: 'someCredentialValue', value: 'CI-0101 someCredentialValue' }, - ]); + await e2eObtainCredential( + credentialDefinitionId, + 'CI-0101 Test Message', + 'CI Issuer', + [ + { name: 'firstName', value: 'CI-0101 firstName' }, + { name: 'lastName', value: 'CI-0101 lastName' }, + { name: 'someCredentialValue', value: 'CI-0101 someCredentialValue' }, + ], + ); }); it('CI-0102 Remove in progress credential from wallet and issuer', async function () { // Reuse the CI-0101 schema and credential definitions - const schemaId = await e2eGetSchemaId('CI-0101 Test Schema'); + const credentialDefinitionId = await e2eGetCredentialDefinitionId( + 'CI-0101_Test_Credential_Definition', + ); - const credentialThread = await e2eSendCredentialProposal( - schemaId, + await e2eSendCredentialProposal( + credentialDefinitionId, 'CI-0102 Test Message', 'CI Issuer', [ @@ -116,10 +123,13 @@ describe('Credential Issuance', function () { // so that if an in-progress request is deleted by the Holder it is // automatically removed from the Issuer. await e2eNavigateToRequestedCredentialsCard(); - await e2eCheckTableDataPresent('CredentialRequestsList', credentialThread); + await e2eCheckTableDataPresent( + 'CredentialRequestsList', + credentialDefinitionId, + ); await e2eExecuteTableRowDropdownAction( 'CredentialRequestsList', - credentialThread, + credentialDefinitionId, 'Actions', 'Cancel Request', 'Abort', @@ -127,39 +137,44 @@ describe('Credential Issuance', function () { ); await e2eCheckTableDataNotPresent( 'CredentialRequestsList', - credentialThread, + credentialDefinitionId, ); await e2eNavigateToActiveCredentialRequestsCard(); await e2eCheckTableDataNotPresent( - 'CredentialRequestsList', - credentialThread, + 'ActiveCredentialRequestsList', + credentialDefinitionId, ); }); it('CI-0103 Remove completed credential from wallet and issuer', async function () { // Walk through stages to obtain credential from Issuer const schemaId = await e2eCreateSchemaDefinition( - 'CI-0103 Test Schema', + 'CI-0103_Test_Schema', '1.0', ['firstName', 'lastName', 'someCredentialValue'], ); - await e2eCreateCredentialDefinition( - 'CI-0103 Test Credential Definition', + const credentialDefinitionId = await e2eCreateCredentialDefinition( + 'CI-0103_Test_Credential_Definition', schemaId, ); - await e2eObtainCredential(schemaId, 'CI-0103 Test Message', 'CI Issuer', [ - { name: 'firstName', value: 'CI-0103 firstName' }, - { name: 'lastName', value: 'CI-0103 lastName' }, - { name: 'someCredentialValue', value: 'CI-0103 someCredentialValue' }, - ]); + await e2eObtainCredential( + credentialDefinitionId, + 'CI-0103 Test Message', + 'CI Issuer', + [ + { name: 'firstName', value: 'CI-0103 firstName' }, + { name: 'lastName', value: 'CI-0103 lastName' }, + { name: 'someCredentialValue', value: 'CI-0103 someCredentialValue' }, + ], + ); // Delete new credential from wallet await e2eNavigateToOwnedCredentialsCard(); await e2eExecuteTableRowRemoveAction( 'OwnedCredentialsList', - 'CI-0103 Test Credential Definition', + 'CI-0103_Test_Credential_Definition', 'Remove', 'Remove Credential', 'Confirm', @@ -168,13 +183,12 @@ describe('Credential Issuance', function () { ); await e2eNavigateToIssuedCredentialsCard(); - await e2eExecuteTableRowRemoveAction( + await e2eExecuteTableRowDropdownAction( 'IssuedCredentialsList', - 'CI-0103 Test Credential Definition', + 'CI-0103_Test_Credential_Definition', + 'Actions', 'Remove', - 'Remove Completed Credential Issuance Record', 'Confirm', - 'Cancel', 'Credential Exchange Record deleted', ); }); diff --git a/e2e/credentialIssuanceHelpers.ts b/e2e/credentialIssuanceHelpers.ts index a606f18..7863c5f 100644 --- a/e2e/credentialIssuanceHelpers.ts +++ b/e2e/credentialIssuanceHelpers.ts @@ -2,6 +2,7 @@ import { By, until } from 'selenium-webdriver'; import { e2eExecuteTableRowDropdownAction, e2eNavigateToCard, + e2eWaitElementVisible, } from './commonHelpers'; import { driver } from './setup-tests'; @@ -61,8 +62,8 @@ export async function e2eNavigateToIssuedCredentialsCard(): Promise { * This allows tests that may want to do other actions before/instead of * submitting. * - * @param {!string} schemaId the unique schema that defines the credential - * type being requested and defines the attributes required. + * @param {!string} credentialDefinitionId the unique credential definition + * that defines the credential type being requested and defines the attributes required. * @param {!string} message a human readable message intended to be sent * to the issuer explaining the purpose or reason for the credential request * @param {!string} connectionAlias the DIDComm connection alias representign @@ -72,7 +73,7 @@ export async function e2eNavigateToIssuedCredentialsCard(): Promise { * */ export async function e2eEnterCredentialProposalDetails( - schemaId: string, + credentialDefinitionId: string, message: string, connectionAlias: string, attributes: { name: string; value: string }[], @@ -96,12 +97,18 @@ export async function e2eEnterCredentialProposalDetails( ); await ( - await driver.wait(until.elementLocated(By.id('schemaId')), ciWaitDefault) + await driver.wait( + until.elementLocated(By.id('credentialDefinitionId')), + ciWaitDefault, + ) ).click(); await ( - await driver.wait(until.elementLocated(By.id('schemaId')), ciWaitDefault) - ).sendKeys(schemaId); + await driver.wait( + until.elementLocated(By.id('credentialDefinitionId')), + ciWaitDefault, + ) + ).sendKeys(credentialDefinitionId); await ( await driver.wait(until.elementLocated(By.id('message')), ciWaitDefault) @@ -158,25 +165,23 @@ export async function e2eEnterCredentialProposalDetails( * Utility function to enter send a credential proposal * for a requesting Holder. * - * @param {!string} schemaId the unique schema that defines the credential - * type being requested and defines the attributes required. + * @param {!string} credentialDefinitionId the unique credential definition + * that defines the credential type being requested and defines the attributes required. * @param {!string} message a human readable message intended to be sent * to the issuer explaining the purpose or reason for the credential request * @param {!string} connectionAlias the DIDComm connection alias representign * connection to the Issuer * @param {!{name:string, value:string}[]} attributes the list of name, value * pairs representing schema attributes and the values to be assigned - * @return {!string} returns the threadid created to identify the credential - * issuance protocol session */ export async function e2eSendCredentialProposal( - schemaId: string, + credentialDefinitionId: string, message: string, connectionAlias: string, attributes: { name: string; value: string }[], -): Promise { +): Promise { await e2eEnterCredentialProposalDetails( - schemaId, + credentialDefinitionId, message, connectionAlias, attributes, @@ -210,35 +215,21 @@ export async function e2eSendCredentialProposal( // Wait for message to dissapear to avoid artifact issues // with subsequent UI actions await driver.wait(until.stalenessOf(confirmation), ciWaitDefault); - - // Get the thread id to use in looking up the - // credential proposal in the issuer - const credentialThread = await ( - await driver.wait( - until.elementLocated( - By.xpath(`//td[contains(.,'${connectionAlias}')]/../td[2]`), - ), - ciWaitDefault, - ) - ).getText(); - - return credentialThread; } /** * Utility test routine to Navigate to Credential Issuer Active Credential * Requests and offer proposed credential. * - * @param {!string} credentialThread the unique id of the credential - * request thread to be offered. + * @param {!string} credentialRowId a unique data value to identify the row */ export async function e2eOfferCredential( - credentialThread: string, + credentialRowId: string, ): Promise { await e2eNavigateToActiveCredentialRequestsCard(); await e2eExecuteTableRowDropdownAction( 'ActiveCredentialRequestsList', - credentialThread, + credentialRowId, 'Actions', 'Offer Credential', 'Offer', @@ -249,16 +240,15 @@ export async function e2eOfferCredential( /** * Utility test routine to accept an offered credential at the Holder * - * @param {!string} credentialThread the unique id of the credential - * request thread to be accepted. + * @param {!string} credentialRowId a unique data value to identify the row */ export async function e2eAcceptCredential( - credentialThread: string, + credentialRowId: string, ): Promise { await e2eNavigateToRequestedCredentialsCard(); await e2eExecuteTableRowDropdownAction( 'CredentialRequestsList', - credentialThread, + credentialRowId, 'Actions', 'Accept Offer', 'Accept', @@ -269,16 +259,15 @@ export async function e2eAcceptCredential( /** * Utility test routine to issue an offered credential at the Issuer * - * @param {!string} credentialThread the unique id of the credential - * request thread to be accepted. + * @param {!string} credentialRowId a unique data value to identify the row */ export async function e2eIssueCredential( - credentialThread: string, + credentialRowId: string, ): Promise { await e2eNavigateToActiveCredentialRequestsCard(); await e2eExecuteTableRowDropdownAction( 'ActiveCredentialRequestsList', - credentialThread, + credentialRowId, 'Actions', 'Issue Credential', 'Issue', @@ -289,16 +278,15 @@ export async function e2eIssueCredential( /** * Utility test routine to save an issued credential at the Holder * - * @param {!string} credentialThread the unique id of the credential - * request thread to be accepted. + * @param {!string} credentialRowId a unique data value to identify the row */ export async function e2eSaveCredential( - credentialThread: string, + credentialRowId: string, ): Promise { await e2eNavigateToRequestedCredentialsCard(); await e2eExecuteTableRowDropdownAction( 'CredentialRequestsList', - credentialThread, + credentialRowId, 'Actions', 'Save Credential', 'Save', @@ -309,10 +297,10 @@ export async function e2eSaveCredential( /** * Utility test routine to execute the complete process of proposing, * issuing and saving a credential based on a specified connnection, - * schema and set of attribute values. + * credential definition and set of attribute values. * - * @param {!string} schemaId the unique schema that defines the credential - * type being requested and defines the attributes required. + * @param {!string} credentialDefinitionId the unique credential definition + * that defines the credential type being requested and defines the attributes required. * @param {!string} message a human readable message intended to be sent * to the issuer explaining the purpose or reason for the credential request * @param {!string} connectionAlias the DIDComm connection alias representing @@ -323,20 +311,46 @@ export async function e2eSaveCredential( * issuance protocol session */ export async function e2eObtainCredential( - schemaId: string, + credentialDefinitionId: string, message: string, connectionAlias: string, attributes: { name: string; value: string }[], -): Promise { - const credentialThread = await e2eSendCredentialProposal( - schemaId, +): Promise> { + await e2eSendCredentialProposal( + credentialDefinitionId, message, connectionAlias, attributes, ); - await e2eOfferCredential(credentialThread); - await e2eAcceptCredential(credentialThread); - await e2eIssueCredential(credentialThread); - await e2eSaveCredential(credentialThread); + await e2eOfferCredential(credentialDefinitionId); + await e2eAcceptCredential(credentialDefinitionId); + await e2eIssueCredential(credentialDefinitionId); + await e2eSaveCredential(credentialDefinitionId); + + // Return the credential exchange record in case they want to + // explicitely reference the records for this exchange later. + await e2eNavigateToIssuedCredentialsCard(); + + await ( + await e2eWaitElementVisible( + By.xpath( + `//div[@id='IssuedCredentialsList']//table/tbody/tr/td[contains(.,'${credentialDefinitionId}')]/../td/span[contains(@class,'anticon-plus-square')]`, + ), + ciWaitDefault, + ) + ).click(); + + const credentialExchange = JSON.parse( + await ( + await e2eWaitElementVisible( + By.xpath( + `//div[@id='IssuedCredentialsList']//table/tbody/tr[contains(@class,'ant-table-expanded-row')]//pre`, + ), + ciWaitDefault, + ) + ).getText(), + ); + + return credentialExchange; } diff --git a/e2e/credentialRevocation.test.ts b/e2e/credentialRevocation.test.ts new file mode 100644 index 0000000..abaffd7 --- /dev/null +++ b/e2e/credentialRevocation.test.ts @@ -0,0 +1,252 @@ +import { By } from 'selenium-webdriver'; +import { + e2eExecuteTableRowDropdownAction, + e2eWaitElementVisible, +} from './commonHelpers'; +import { e2eAcceptInvitation, e2eCreateInvitation } from './connectionHelpers'; +import { + e2eCreateCredentialDefinition, + e2eGetCredentialDefinitionId, +} from './credentialDefinitionHelpers'; +import { + e2eNavigateToIssuedCredentialsCard, + e2eObtainCredential, +} from './credentialIssuanceHelpers'; +import { + e2eSendProofPresentation, + e2eSendProofRequest, + e2eVerifyProof, +} from './proofPresentationHelpers'; +import { e2eCreateSchemaDefinition } from './schemaDefinitionHelpers'; + +export const crWaitDefault = 40000; + +// A global to hold credential details across tests within this +// suite. +let CR0101_Credential_Exchange_Details: Record; + +describe('Credential Revocation', function () { + it('CR-0101 Complete credential revocation', async function () { + // Need to create schema annd credential definitions as + // well as a DIDComm connection to the issuer first to + // get a valid schema identifier + const invitationText = await e2eCreateInvitation('CR Holder'); + await e2eAcceptInvitation('CR Issuer', invitationText); + + const schemaId = await e2eCreateSchemaDefinition( + 'CR-0101_Test_Schema', + '1.0', + ['firstName', 'lastName', 'someCredentialValue'], + ); + + const credentialDefinitionId = await e2eCreateCredentialDefinition( + 'CR-0101_Test_Credential_Definition', + schemaId, + true, + '10000', + '1000', + ); + + CR0101_Credential_Exchange_Details = await e2eObtainCredential( + credentialDefinitionId, + 'CR-0101 Test Message', + 'CR Issuer', + [ + { name: 'firstName', value: 'CR-0101 firstName' }, + { name: 'lastName', value: 'CR-0101 lastName' }, + { name: 'someCredentialValue', value: 'CR-0101 someCredentialValue' }, + ], + ); + + // Revoke the credential via the issuers IssuedCredentials table and + // check that the status is updated from active to revoked + await e2eNavigateToIssuedCredentialsCard(); + + await e2eWaitElementVisible( + By.xpath( + `//div[@id='IssuedCredentialsList']//table/tbody/tr/td[contains(.,'CR-0101_Test_Credential_Definition')]/..//*[contains(@class,'anticon anticon-safety-certificate')]`, + ), + crWaitDefault, + ); + + // Must wait a few seconds so that the credential can be checked + // as valid at some point in time. + await new Promise((r) => setTimeout(r, 15000)); + + await e2eExecuteTableRowDropdownAction( + 'IssuedCredentialsList', + 'CR-0101_Test_Credential_Definition', + 'Actions', + 'Revoke Credential', + 'Revoke', + 'Revoked', + ); + + await e2eWaitElementVisible( + By.xpath( + `//div[@id='IssuedCredentialsList']//table/tbody/tr/td[contains(.,'CR-0101_Test_Credential_Definition')]/..//*[contains(@class,'anticon anticon-close-square')]`, + ), + crWaitDefault, + ); + }); + + it('CI-0201 Attempt to revoke non-revocable credential', async function () { + const schemaId = await e2eCreateSchemaDefinition( + 'CR-0201_Test_Schema', + '1.0', + ['firstName', 'lastName', 'someCredentialValue'], + ); + + // Create a non-revocable credential definition + const credentialDefinitionId = await e2eCreateCredentialDefinition( + 'CR-0201_Test_Credential_Definition', + schemaId, + false, + ); + + await e2eObtainCredential( + credentialDefinitionId, + 'CR-0201 Test Message', + 'CR Issuer', + [ + { name: 'firstName', value: 'CR-0201 firstName' }, + { name: 'lastName', value: 'CR-0201 lastName' }, + { name: 'someCredentialValue', value: 'CR-0201 someCredentialValue' }, + ], + ); + + // Attempt to use action dropdown on IssuedCredential table to revoke + // credential and check that revoke option is disabled. + await e2eNavigateToIssuedCredentialsCard(); + + await e2eWaitElementVisible( + By.xpath( + `//div[@id='IssuedCredentialsList']//table/tbody/tr/td[contains(.,'CR-0201_Test_Credential_Definition')]/..//*[contains(@class,'anticon anticon-safety-certificate')]`, + ), + crWaitDefault, + ); + + await ( + await e2eWaitElementVisible( + By.xpath( + `//div[@id='IssuedCredentialsList']//table/tbody/tr/td[contains(.,'CR-0201_Test_Credential_Definition')]/..//button[contains(.,'Action')]`, + ), + crWaitDefault, + ) + ).click(); + + { + const element = await e2eWaitElementVisible( + By.xpath(`//li[contains(.,'Revoke Credential')]`), + crWaitDefault, + ); + expect(await element.getAttribute('aria-disabled')).toBeTruthy(); + } + }); + + it('CR-0202 Proof presentation failure using revoked credential', async function () { + // Because this is part of the same test suite, we will re-use the credential already issued + // in CR-0101 since it reduces significant pre-work and run time + const credentialDefinitionId = await e2eGetCredentialDefinitionId( + 'CR-0101_Test_Credential_Definition', + ); + + // We know that the credential issued for this was revoked in + // CR-0101. There is an assumption that there has at least been + // 5 seconds between that revocation and now since we need to + // provide a time range that will be outside that which the + // credential was valid. + const proofRequestThreadId = await e2eSendProofRequest( + credentialDefinitionId, + 'CR-0202 Proof Request Test Message', + 'CR Holder', + [ + { + name: 'firstName', + toggleRequired: false, + timeRange: { + start: new Date(Date.now()), + end: new Date(Date.now()), + }, + }, + { + name: 'lastName', + toggleRequired: false, + timeRange: { + start: new Date(Date.now()), + end: new Date(Date.now()), + }, + }, + { + name: 'someCredentialValue', + toggleRequired: false, + timeRange: { + start: new Date(Date.now()), + end: new Date(Date.now()), + }, + }, + ], + ); + await e2eSendProofPresentation(proofRequestThreadId, [ + { name: 'firstName', toggleReveal: false }, + { name: 'lastName', toggleReveal: false }, + { name: 'someCredentialValue', toggleReveal: false }, + ]); + await e2eVerifyProof(proofRequestThreadId, false); + }); + + it('CR-0203 Proof presentation success for time when valid using revoked credential', async function () { + // Because this is part of the same test suite, we will re-use the credential already invoked + // in CR-0101 since it reduces significant pre-work and run time + const credentialDefinitionId = await e2eGetCredentialDefinitionId( + 'CR-0101_Test_Credential_Definition', + ); + + // We know that the credential issued for this was revoked in + // CR-0101. We can get the creation time from the credential + // offer and use that time with a 3 second window that it should + // have been valid for before it was revoked + const startTime = + new Date(CR0101_Credential_Exchange_Details.record.updated_at).valueOf() + + 2000; + const endTime = startTime + 5000; + + const proofRequestThreadId = await e2eSendProofRequest( + credentialDefinitionId, + 'CR-0203 Proof Request Test Message', + 'CR Holder', + [ + { + name: 'firstName', + toggleRequired: false, + timeRange: { + start: new Date(startTime), + end: new Date(endTime), + }, + }, + { + name: 'lastName', + toggleRequired: false, + timeRange: { + start: new Date(startTime), + end: new Date(endTime), + }, + }, + { + name: 'someCredentialValue', + toggleRequired: false, + timeRange: { + start: new Date(startTime), + end: new Date(endTime), + }, + }, + ], + ); + await e2eSendProofPresentation(proofRequestThreadId, [ + { name: 'firstName', toggleReveal: false }, + { name: 'lastName', toggleReveal: false }, + { name: 'someCredentialValue', toggleReveal: false }, + ]); + await e2eVerifyProof(proofRequestThreadId, true); + }); +}); diff --git a/e2e/docker/docker-compose-test.yml b/e2e/docker/docker-compose-test.yml new file mode 100644 index 0000000..fb8a47a --- /dev/null +++ b/e2e/docker/docker-compose-test.yml @@ -0,0 +1,37 @@ +# This docker compose file will setup the browser +# test container and admin console webserver for e2e +# testing once the di-env environment is up. +# +version: '3' +services: + # A docker image that runs the selenium chrome browser server + browser-server: + image: ${BROWSER_SERVER_DOCKER_IMAGE:-selenium/standalone-chrome-debug}:${BROWSER_SERVER_DOCKER_TAG:-3.141.59} + container_name: ${BROWSER_SERVER_CONTAINER_NAME:-browser-server} + ports: + - ${BROWSER_SERVER_CONTROL_EXTERNAL_PORT:-4445}:${BROWSER_SERVER_CONTROL_INTERNAL_PORT:-4444} + - ${BROWSER_SERVER_VNC_EXTERNAL_PORT:-5900}:${BROWSER_SERVER_VNC_INTERNAL_PORT:-5900} + environment: + - SCREEN_WIDTH=1600 + - SCREEN_HEIGHT=1200 + networks: + - frontend + volumes: + - '/dev/shm:/dev/shm' + + # An nginx webserver image to serve admin console application build + admin-webserver: + image: ${WEB_SERVER_DOCKER_IMAGE:-nginx}:${WEB_SERVER_DOCKER_TAG:-1.21.3-alpine} + container_name: ${WEB_SERVER_CONTAINER_NAME:-admin-webserver} + ports: + - ${WEB_SERVER_EXTERNAL_PORT:-3000}:${WEB_SERVER_INTERNAL_PORT:-80} + networks: + - frontend + volumes: + - ${BUILD_DIR:-./../../build}:/usr/share/nginx/html:ro + - ./nginx-default.conf:/etc/nginx/conf.d/default.conf +networks: + # Externally defined di-env network that the cloud-agent is attached to. + frontend: + external: ${FRONTEND_EXTERNAL:-true} + name: ${FRONTEND_NETWORK:-frontend} diff --git a/e2e/docker/nginx-default.conf b/e2e/docker/nginx-default.conf new file mode 100644 index 0000000..9d7758a --- /dev/null +++ b/e2e/docker/nginx-default.conf @@ -0,0 +1,48 @@ +server { + listen 80; + listen [::]:80; + server_name localhost; + + #access_log /var/log/nginx/host.access.log main; + + location / { + root /usr/share/nginx/html; + index index.html index.html; + # This line is needed to work with React routing + # otherwise browser reload results in 404 errors + try_files $uri /index.html; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + # proxy the PHP scripts to Apache listening on 127.0.0.1:80 + # + #location ~ \.php$ { + # proxy_pass http://127.0.0.1; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # root html; + # fastcgi_pass 127.0.0.1:9000; + # fastcgi_index index.php; + # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} + diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts deleted file mode 100644 index c6f25f5..0000000 --- a/e2e/global-setup.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Server } from 'http'; -import { serve } from './service'; - -// Executed only once at the start of a e2e test run sequence -export const mutableGlobals = { server: new Server() }; - -export default async function (): Promise { - mutableGlobals.server = await serve(3000); -} diff --git a/e2e/global-teardown.ts b/e2e/global-teardown.ts deleted file mode 100644 index f62f629..0000000 --- a/e2e/global-teardown.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Server } from 'http'; -import { mutableGlobals } from './global-setup'; - -// Executed only once at the end of a e2e test run sequence -export default async function (): Promise { - const server: Server = mutableGlobals.server; - if (server) { - server.close(); - } -} diff --git a/e2e/proofPresentation.test.ts b/e2e/proofPresentation.test.ts index 2c97bd0..25c85d1 100644 --- a/e2e/proofPresentation.test.ts +++ b/e2e/proofPresentation.test.ts @@ -7,7 +7,10 @@ import { e2eWaitElementVisible, } from './commonHelpers'; import { e2eAcceptInvitation, e2eCreateInvitation } from './connectionHelpers'; -import { e2eCreateCredentialDefinition } from './credentialDefinitionHelpers'; +import { + e2eCreateCredentialDefinition, + e2eGetCredentialDefinitionId, +} from './credentialDefinitionHelpers'; import { e2eObtainCredential } from './credentialIssuanceHelpers'; import { e2eNavigateToHolderActiveProofPresentationsCard, @@ -19,10 +22,7 @@ import { e2eVerifyProof, ppWaitDefault, } from './proofPresentationHelpers'; -import { - e2eCreateSchemaDefinition, - e2eGetSchemaId, -} from './schemaDefinitionHelpers'; +import { e2eCreateSchemaDefinition } from './schemaDefinitionHelpers'; import { driver } from './setup-tests'; describe('Proof Presentation', function () { @@ -78,18 +78,18 @@ describe('Proof Presentation', function () { await e2eAcceptInvitation('PP Issuer', invitationText); const schemaId = await e2eCreateSchemaDefinition( - 'PP-0101 Test Schema', + 'PP-0101_Test_Schema', '1.0', ['firstName', 'lastName', 'someCredentialValue', 'unRequestedAttribute'], ); - await e2eCreateCredentialDefinition( - 'PP-0101 Test Credential Definition', + const credentialDefinitionId = await e2eCreateCredentialDefinition( + 'PP-0101_Test_Credential_Definition', schemaId, ); await e2eObtainCredential( - schemaId, + credentialDefinitionId, 'PP-0101 Credential Request Test Message', 'PP Issuer', [ @@ -106,7 +106,7 @@ describe('Proof Presentation', function () { // Initiate proof request from verifier. In this instance the issuer is // also acting as the verifier as would be a common case for DI currently const proofRequestThreadId = await e2eSendProofRequest( - schemaId, + credentialDefinitionId, 'PP-0101 Proof Request Test Message', 'PP Holder', [ @@ -126,12 +126,14 @@ describe('Proof Presentation', function () { it('PP-0102 Remove in progress proof from wallet and issuer', async function () { // Re-use PP-0101 issued credential - const schemaId = await e2eGetSchemaId('PP-0101 Test Schema'); + const credentialDefinitionId = await e2eGetCredentialDefinitionId( + 'PP-0101_Test_Credential_Definition', + ); // Initiate proof request from verifier. In this instance the issuer is // also acting as the verifier as would be a common case for DI currently const proofRequestThreadId = await e2eSendProofRequest( - schemaId, + credentialDefinitionId, 'PP-0102 Proof Request Test Message', 'PP Holder', [ @@ -165,12 +167,14 @@ describe('Proof Presentation', function () { it('PP-0103 Remove completed proof from wallet and issuer', async function () { // Re-use PP-0101 issued credential - const schemaId = await e2eGetSchemaId('PP-0101 Test Schema'); + const credentialDefinitionId = await e2eGetCredentialDefinitionId( + 'PP-0101_Test_Credential_Definition', + ); // Initiate proof request from verifier. In this instance the issuer is // also acting as the verifier as would be a common case for DI currently const proofRequestThreadId = await e2eSendProofRequest( - schemaId, + credentialDefinitionId, 'PP-0103 Proof Request Test Message', 'PP Holder', [ diff --git a/e2e/proofPresentationHelpers.ts b/e2e/proofPresentationHelpers.ts index 2d277cd..19cd33e 100644 --- a/e2e/proofPresentationHelpers.ts +++ b/e2e/proofPresentationHelpers.ts @@ -1,8 +1,10 @@ import { By, until } from 'selenium-webdriver'; import { e2eActivateTableRowDropdownAction, + e2eEnterDatePickerDetails, e2eExecuteTableRowDropdownAction, e2eNavigateToCard, + e2eWaitElementVisible, } from './commonHelpers'; import { driver } from './setup-tests'; @@ -62,21 +64,27 @@ export async function e2eNavigateToHolderCompletedProofPresentationsCard(): Prom * This allows tests that may want to do other actions before/instead of * submitting. * - * @param {!string} schemaId the unique schema that defines the credential - * type being requested and defines the attributes required. + * @param {!string} credentialDefinitionId the unique credential definition + * that defines the credential type being requested and defines the attributes required. * @param {!string} message a human readable message intended to be sent * to the Holder explaining the purpose or reason for the credential request * @param {!string} connectionAlias the DIDComm connection alias representing * connection to the Holder - * @param {!{name:string, click:boolean}[]} attributes the list of schema - * attributes and an indication for each on whether to toggle the associated checkbox. + * @param {!{name:string, toggleRequired:boolean, timeRange?: {start:Date; end:Date}}[]} attributes + * the list of schema attributes, an indication for each on whether to toggle + * the associated checkbox and an optional Date range that the credential was + * not-revoked * */ export async function e2eEnterProofRequestDetails( - schemaId: string, + credentialDefinitionId: string, message: string, connectionAlias: string, - attributes: { name: string; toggleRequired: boolean }[], + attributes: { + name: string; + toggleRequired: boolean; + timeRange?: { start: Date; end: Date }; + }[], ): Promise { await e2eNavigateToVerifierActiveProofRequestsCard(); await ( @@ -97,11 +105,29 @@ export async function e2eEnterProofRequestDetails( ); await ( - await driver.wait(until.elementLocated(By.id('schemaId')), ppWaitDefault) + await driver.wait( + until.elementLocated(By.id('credentialDefinitionId')), + ppWaitDefault, + ) ).click(); - await ( - await driver.wait(until.elementLocated(By.id('schemaId')), ppWaitDefault) - ).sendKeys(schemaId); + + { + const element = await driver.wait( + until.elementLocated(By.id('credentialDefinitionId')), + ppWaitDefault, + ); + + await driver + .actions({ bridge: true }) + .move({ origin: element }) + .click() + .pause(200) + .sendKeys(credentialDefinitionId) + // Pause to let the credential definition to fetch schema update + .pause(1000) + .perform(); + } + await ( await driver.wait(until.elementLocated(By.id('message')), ppWaitDefault) ).click(); @@ -148,31 +174,91 @@ export async function e2eEnterProofRequestDetails( ); await checkBox.click(); } + // If there is a timewindow for non-revocation + // we need to navigate the range picker + if (attributes[i].timeRange !== undefined) { + // Open the start time picker + await ( + await driver.wait( + until.elementLocated( + By.xpath( + `//div[@id='RequestProofAttributeList']/div/div/table/tbody/tr/td[contains(.,'${attributes[i].name}')]/../td/div[contains(@class,'RequestProofAttributeList__${attributes[i].name}_ValidTimeWindow')]/div[1]/input`, + ), + ), + ppWaitDefault, + ) + ).click(); + + const datePickerLocator = `//div[contains(@class,'RequestProofAttributeList__${attributes[i].name}_DatePicker')]`; + // Input the start value provided + await e2eEnterDatePickerDetails( + datePickerLocator, + attributes[i].timeRange!.start, + ppWaitDefault, + ); + + // Move to the end time picker + await ( + await driver.wait( + until.elementLocated( + By.xpath( + `${datePickerLocator}//div[@class='ant-picker-footer']//button[contains(@class,'ant-btn-primary')]`, + ), + ), + ppWaitDefault, + ) + ).click(); + + // Input the end value provided + await e2eEnterDatePickerDetails( + datePickerLocator, + attributes[i].timeRange!.end, + ppWaitDefault, + ); + + // Close the time range input + await ( + await driver.wait( + until.elementLocated( + By.xpath( + `${datePickerLocator}//div[@class='ant-picker-footer']//button[contains(@class,'ant-btn-primary')]`, + ), + ), + ppWaitDefault, + ) + ).click(); + } } } /** * Utility function to send a proof request from a Verifier to a Holder * - * @param {!string} schemaId the unique schema that defines the credential - * type being requested and defines the attributes required. + * @param {!string} credentialDefinitionId the unique credential definition + * that defines the credential type being requested and defines the attributes required. * @param {!string} message a human readable message intended to be sent * to the Holder explaining the purpose or reason for the credential request * @param {!string} connectionAlias the DIDComm connection alias representing * connection to the Holder - * @param {!{name:string, click:boolean}[]} attributes the list of schema - * attributes and an indication for each on whether to toggle the associated checkbox. + * @param {!{name:string, toggleRequired:boolean, timeRange?: {start:Date; end:Date}}[]} attributes + * the list of schema attributes, an indication for each on whether to toggle + * the associated checkbox and an optional Date range that the credential was + * not-revoked * @returns {string} the threadId that identifies the proof request * */ export async function e2eSendProofRequest( - schemaId: string, + credentialDefinitionId: string, message: string, connectionAlias: string, - attributes: { name: string; toggleRequired: boolean }[], + attributes: { + name: string; + toggleRequired: boolean; + timeRange?: { start: Date; end: Date }; + }[], ): Promise { await e2eEnterProofRequestDetails( - schemaId, + credentialDefinitionId, message, connectionAlias, attributes, @@ -210,7 +296,7 @@ export async function e2eSendProofRequest( const proofRequestThread = await ( await driver.wait( until.elementLocated( - By.xpath(`//td[contains(.,'${connectionAlias}')]/../td[2]`), + By.xpath(`//td[contains(.,'${connectionAlias}')]/../td[4]`), ), ppWaitDefault, ) @@ -333,8 +419,13 @@ export async function e2eSendProofPresentation( * * @param {!string} proofThread the unique id of the proof * request thread to be verified. + * @param {?boolean} shouldFail indicates if the verification result should be + * a failure. */ -export async function e2eVerifyProof(proofThread: string): Promise { +export async function e2eVerifyProof( + proofThread: string, + shouldSucceed?: boolean, +): Promise { await e2eNavigateToVerifierActiveProofRequestsCard(); await e2eExecuteTableRowDropdownAction( 'ActiveProofRequestsList', @@ -344,4 +435,23 @@ export async function e2eVerifyProof(proofThread: string): Promise { 'Verify', 'Proof Verified', ); + + // Refresh to make sure we have updates and then check the + // verification result is as requested + await e2eNavigateToVerifierActiveProofRequestsCard(); + if (shouldSucceed === false) { + await e2eWaitElementVisible( + By.xpath( + `//div[@id='CompletedProofsList']//table/tbody/tr/td[contains(.,'${proofThread}')]/../td/span[contains(@class,'anticon-dislike')]`, + ), + ppWaitDefault, + ); + } else { + await e2eWaitElementVisible( + By.xpath( + `//div[@id='CompletedProofsList']//table/tbody/tr/td[contains(.,'${proofThread}')]/../td/span[contains(@class,'anticon-like')]`, + ), + ppWaitDefault, + ); + } } diff --git a/e2e/schemaDefinition.test.ts b/e2e/schemaDefinition.test.ts index 1b5d190..b6af0dc 100644 --- a/e2e/schemaDefinition.test.ts +++ b/e2e/schemaDefinition.test.ts @@ -31,20 +31,20 @@ describe('Schema Definition', function () { }); it('SD-0101 Create valid basic schema with one attribute', async function () { - await e2eCreateSchemaDefinition('SD-0101 Test Schema', '0.1', [ + await e2eCreateSchemaDefinition('SD-0101_Test_Schema', '0.1', [ 'licenseNumber', ]); }); it('SD-0102 Create valid basic schema with two attributes', async function () { - await e2eCreateSchemaDefinition('SD-0102 Test Schema', '1.0', [ + await e2eCreateSchemaDefinition('SD-0102_Test_Schema', '1.0', [ 'firstName', 'lastName', ]); }); it('SD-0103 Enter valid schema details with two pages of attributes check paging updates', async function () { - await e2eEnterSchemaDefintionDetails('SD-0103 Test Schema', '1.5', [ + await e2eEnterSchemaDefintionDetails('SD-0103_Test_Schema', '1.5', [ 'firstName', 'lastName', 'streetNumber', @@ -72,7 +72,7 @@ describe('Schema Definition', function () { }); it('SD-0104 Initiate valid basic schema with one attribute then delete attribute', async function () { - await e2eEnterSchemaDefintionDetails('SD-0104 Test Schema', '0.2', [ + await e2eEnterSchemaDefintionDetails('SD-0104_Test_Schema', '0.2', [ 'licenseNumber', ]); diff --git a/e2e/service.ts b/e2e/service.ts deleted file mode 100644 index 8d33515..0000000 --- a/e2e/service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import path from 'path'; -import express from 'express'; -import { Server } from 'http'; - -export async function serve(port: number): Promise { - const app = express(); - - // Serve all static assets - app.use(express.static('build')); - - // Serve index.html for all other resources - app.get('*', (_req, res) => { - res.sendFile(path.join(__dirname, '../build/index.html')); - }); - - // Start server - const server = app.listen(port); - - console.log(`\nWeb server is running on 'http://localhost:${port}`); - - return server; -} diff --git a/e2e/setup-tests.ts b/e2e/setup-tests.ts index 5c3370b..5c46fc5 100644 --- a/e2e/setup-tests.ts +++ b/e2e/setup-tests.ts @@ -12,7 +12,7 @@ import { env } from './config'; expect.extend({ toMatchImageSnapshot }); // increased default timeout to allow for slow running / complex tests -jest.setTimeout(120000); +jest.setTimeout(1500000); export let driver: WebDriver; beforeEach(async () => { diff --git a/jest.e2e.config.js b/jest.e2e.config.js index e5deae0..939b44a 100644 --- a/jest.e2e.config.js +++ b/jest.e2e.config.js @@ -1,23 +1,15 @@ module.exports = { rootDir: '.', testEnvironment: 'node', - testMatch: [ - '/e2e/**/*.test.ts', - ], - globalSetup: '/e2e/global-setup.ts', - globalTeardown: '/e2e/global-teardown.ts', + testMatch: ['/e2e/**/*.test.ts'], setupFilesAfterEnv: ['/e2e/setup-tests.ts'], coverageDirectory: 'coverage-e2e', collectCoverageFrom: [ - "/src/**/*.{ts,tsx}", - "!/**/*.d.ts", - "!/src/test/**/*.{ts,tsx}", - "!/e2e/**/*.{ts,tsx}", - "!/src/**/*.spec.{ts,tsx}", - ], - coverageReporters: [ - "html", - "text", - "text-summary" + '/src/**/*.{ts,tsx}', + '!/**/*.d.ts', + '!/src/test/**/*.{ts,tsx}', + '!/e2e/**/*.{ts,tsx}', + '!/src/**/*.spec.{ts,tsx}', ], + coverageReporters: ['html', 'text', 'text-summary'], }; diff --git a/package.json b/package.json index a0d0d5f..a960387 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@sudoplatform-labs/sudo-di-cloud-agent-admin-console", "description": "Sudo Decentalized Identity Cloud Agent Admin Console Application", - "version": "0.3.0", + "version": "0.4.0", "author": "Anonyome Platform Team", "license": "Apache-2.0", "files": [ @@ -18,14 +18,14 @@ "build": "react-scripts build", "build:prod": "yarn audit && yarn build", "build:ts-check": "yarn tsc --noEmit", - "test:update-acapy-address": "./utils/update-acapy-address.sh -c ./build/acapy.json", + "test:update-acapy-address": "./utils/update-acapy-address.sh -c ./build/acapy.json -h cloud-agent", "test:execute-e2e": "jest --runInBand -c jest.e2e.config.js", - "test:start-browser-server-debug": "./e2e/browser-server.sh start -i selenium/standalone-chrome-debug:3.141.59 -p 4445", - "test:start-browser-server": "./e2e/browser-server.sh start -i selenium/standalone-chrome:3.141.59 -p 4445", - "test:stop-browser-server": "./e2e/browser-server.sh stop", - "test:start-e2e-env": "yarn di-env start -c $PWD/public/acapy.json && yarn test:update-acapy-address && yarn test:start-browser-server-debug", - "test:stop-e2e-env": "yarn test:stop-browser-server; yarn di-env down", - "test:run-e2e": "rm -rf .e2e_test_success && yarn test:start-e2e-env && yarn test:execute-e2e && touch .e2e_test_success; yarn test:stop-e2e-env", + "test:up-browser-server-debug": "./e2e/browser-server.sh up -i selenium/standalone-chrome-debug:3.141.59 -p 4445", + "test:up-browser-server": "./e2e/browser-server.sh up -i selenium/standalone-chrome:3.141.59 -p 4445", + "test:down-browser-server": "./e2e/browser-server.sh down", + "test:up-e2e-env": "yarn di-env up -c $PWD/public/acapy.json && yarn test:update-acapy-address && yarn test:up-browser-server-debug", + "test:down-e2e-env": "yarn test:down-browser-server; yarn di-env down", + "test:run-e2e": "rm -rf .e2e_test_success && yarn test:up-e2e-env && yarn test:execute-e2e && touch .e2e_test_success; yarn test:down-e2e-env", "test:unit": "yarn react-scripts test", "test:e2e": "yarn test:run-e2e", "test:all": "CI=true yarn test:unit && yarn test:e2e ", @@ -43,10 +43,10 @@ }, "dependencies": { "@ant-design/icons": "^4.0.5", - "@sudoplatform-labs/sudo-di-cloud-agent": "^0.4.0", - "antd": "^4.5.4", + "@sudoplatform-labs/sudo-di-cloud-agent": "^0.6.1", + "antd": "4.9.4", "antd-mask-input": "^0.1.13", - "color": "^3.1.2", + "color": "^3.2.0", "d3": "^5.12.0", "https-proxy-agent": "^5.0.0", "lodash": "^4.17.21", @@ -118,7 +118,7 @@ "react-test-renderer": "^16.14.0", "selenium-webdriver": "^4.0.0-alpha.8", "tsconfig-paths": "^3.9.0", - "typescript": "~3.7.2" + "typescript": "~4.4.3" }, "resolutions": { "**/node-fetch": "^2.6.1", @@ -127,6 +127,7 @@ "**/lodash": "^4.17.21", "**/hosted-git-info": "^2.8.9", "**/url-parse": "^1.5.0", + "**/path-parse": "^1.0.7", "react-scripts/webpack/terser-webpack-plugin/cacache/ssri": "^6.0.2", "react-scripts/terser-webpack-plugin/cacache/ssri": "^8.0.1", "react-scripts/react-dev-utils/immer": "^8.0.1", @@ -138,11 +139,6 @@ "!**/*.d.ts", "!src/test/**/*.{ts,tsx}" ], - "coverageThreshold": { - "global": { - "statements": 40 - } - }, "coverageReporters": [ "text", "text-summary", diff --git a/src/components/Form/DIDCommSelectionItem.tsx b/src/components/Form/DIDCommSelectionItem.tsx index 8cf9381..dd22218 100644 --- a/src/components/Form/DIDCommSelectionItem.tsx +++ b/src/components/Form/DIDCommSelectionItem.tsx @@ -14,7 +14,7 @@ interface Props { cloudAgentAPIs: CloudAgentAPI; } -// This JSX.element is intended to be included as a Form.Item +// This function returning a JSX.element is intended to be included as a Form.Item // inside a Form. It provides a dropdown selection of // DIDComm connections known to the Agent. export const DIDCommSelectionItem = (props: Props): JSX.Element => { diff --git a/src/components/Form/IndyCredentialDefinitionIdEntryItem.tsx b/src/components/Form/IndyCredentialDefinitionIdEntryItem.tsx new file mode 100644 index 0000000..bff009a --- /dev/null +++ b/src/components/Form/IndyCredentialDefinitionIdEntryItem.tsx @@ -0,0 +1,53 @@ +import { Form, Input } from 'antd'; +import { RuleObject } from 'antd/lib/form'; +import { StoreValue } from 'antd/lib/form/interface'; +import React from 'react'; + +interface Props { + name: string; + setCredentialDefinitionId?: ( + credentialId: React.SetStateAction, + ) => void; +} + +// This JSX.element is intended to be included as a Form.Item +// inside a Form. It provides a text entry box and validation +// for Indy format Credential Identifiers. +export const IndyCredentialDefinitionIdEntryItem = ( + props: Props, +): JSX.Element => { + return ( + => { + const validCredentialIdRegex = + /^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)$/; + // Vaidate that the pattern matches a valid schema Id before attempting + // to fetch it to avoid constant fetch attempts on every character entry. + if (value && validCredentialIdRegex.test(value)) { + if (props.setCredentialDefinitionId) { + props.setCredentialDefinitionId(value); + } + return Promise.resolve(value); + } else { + return Promise.reject( + 'Please enter a valid Credential Definition Identifier', + ); + } + }, + }, + ]}> + + + ); +}; diff --git a/src/components/Form/IndySchemaIdEntryItem.tsx b/src/components/Form/IndySchemaIdEntryItem.tsx index db30921..db82ff8 100644 --- a/src/components/Form/IndySchemaIdEntryItem.tsx +++ b/src/components/Form/IndySchemaIdEntryItem.tsx @@ -23,7 +23,8 @@ export const IndySchemaIdEntryItem = (props: Props): JSX.Element => { message: 'Please provide a Schema Identifier for the proof type required.', validator: (rule: RuleObject, value: StoreValue): Promise => { - const validSchemaIdRegex = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9]+\.[0-9]+$/; + const validSchemaIdRegex = + /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9]+\.[0-9]+$/; // Vaidate that the pattern matches a valid schema Id before attempting // to fetch it to avoid constant fetch attempts on every character entry. if (value && validSchemaIdRegex.test(value)) { diff --git a/src/components/Proofs/CompletedProofsCard.tsx b/src/components/Proofs/CompletedProofsCard.tsx index c2d25eb..026419e 100644 --- a/src/components/Proofs/CompletedProofsCard.tsx +++ b/src/components/Proofs/CompletedProofsCard.tsx @@ -1,13 +1,12 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { message, Popover } from 'antd'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect } from 'react'; import { useAsyncFn } from 'react-use'; import styled from 'styled-components'; import { ConsoleCard } from '../../components/ConsoleCard'; import { CardsCol, CardsRow } from '../../components/NavLayout'; import { theme } from '../../theme'; import { AppContext } from '../../containers/App'; -import { useInterval } from '../../utils/intervals'; import { deleteProofExchange, fetchFilteredProofExchangeRecords, @@ -15,8 +14,10 @@ import { } from '../../models/ACAPy/ProofPresentation'; import { CompletedProofsList } from '../../components/Proofs/CompletedProofsList'; import { fetchAllAgentConnectionDetails } from '../../models/ACAPy/Connections'; -import { HStack } from '../layout-stacks'; -import { PresentProofRecordsGetRoleEnum, PresentProofRecordsGetStateEnum } from '@sudoplatform-labs/sudo-di-cloud-agent'; +import { + PresentProofRecordsGetRoleEnum, + PresentProofRecordsGetStateEnum, +} from '@sudoplatform-labs/sudo-di-cloud-agent'; /** * Props define the agent role for this card instance @@ -69,7 +70,10 @@ export const CompletedProofsCard: React.FC = (props) => { cloudAgentAPIs, { role: role, - states: [ PresentProofRecordsGetStateEnum.Verified, PresentProofRecordsGetStateEnum.PresentationAcked ], + states: [ + PresentProofRecordsGetStateEnum.Verified, + PresentProofRecordsGetStateEnum.PresentationAcked, + ], }, ); const connections = await fetchAllAgentConnectionDetails(cloudAgentAPIs); @@ -92,17 +96,6 @@ export const CompletedProofsCard: React.FC = (props) => { getProofsCompletedInfo(); }, [getProofsCompletedInfo]); - // Slow poll for any proof table changes since - // we don't have any ACA-py hooks implemented. - const [count, setCount] = useState(30); - useInterval(() => { - setCount(count - 2); - if (count <= 0) { - setCount(30); - getProofsCompletedInfo(); - } - }, 2000); - const deleteProofPresentationHandler = useCallback( async (presentationId: string) => { try { @@ -126,6 +119,7 @@ export const CompletedProofsCard: React.FC = (props) => { ); @@ -140,11 +134,6 @@ export const CompletedProofsCard: React.FC = (props) => { Completed Proof Presentations - } - extra={ - -
Refresh in {count.toString().padStart(2, '0')}
-
}> {proofsData} diff --git a/src/components/Proofs/CompletedProofsList.tsx b/src/components/Proofs/CompletedProofsList.tsx index c1bff2e..9d4b0cf 100644 --- a/src/components/Proofs/CompletedProofsList.tsx +++ b/src/components/Proofs/CompletedProofsList.tsx @@ -8,7 +8,13 @@ import { DangerLink, ActionHandler, } from '../table'; -import { PlusSquareOutlined, MinusSquareOutlined } from '@ant-design/icons'; +import { + PlusSquareOutlined, + MinusSquareOutlined, + WarningTwoTone, + LikeTwoTone, + DislikeTwoTone, +} from '@ant-design/icons'; import { VStack } from '../layout-stacks'; import { Heading } from '../charts'; import { @@ -17,6 +23,8 @@ import { } from '../../models/ACAPy/ProofPresentation'; import { modalDanger } from '../Form'; import { convertAriesDateToLocal } from '../../utils/ariesDate'; +import { PresentProofRecordsGetRoleEnum } from '@sudoplatform-labs/sudo-di-cloud-agent'; +import { theme } from '../../theme'; const CompletedProofsInfoTable = Table as React.FC< TableProps @@ -36,11 +44,12 @@ const StyledConsoleTable = styled(CompletedProofsInfoTable)` interface Props { dataSource: PresentationExchangeData[]; loading?: boolean; + role: PresentProofRecordsGetRoleEnum; onDelete: ActionHandler; } export const CompletedProofsList: React.FC = (props) => { - const { dataSource, loading, onDelete: doRemove } = props; + const { dataSource, loading, role, onDelete: doRemove } = props; const [searchState, setSearchState] = useState({ searchText: '', @@ -48,19 +57,43 @@ export const CompletedProofsList: React.FC = (props) => { }); const makeColumns = (opts: { + role: PresentProofRecordsGetRoleEnum; onRemove: ActionHandler; }): ColumnProps[] => { - return [ - { - title: 'Thread', - dataIndex: ['record', 'thread_id'], + let proofResult: ColumnProps; + if (role === 'verifier') { + // Only the verifier ever knows the success of the + // proof verification process. + proofResult = { + title: 'Status', + width: '10%', + align: 'center', + render(_, proofInfo) { + if (proofInfo.record.verified !== undefined) { + if (proofInfo.record.verified === 'true') { + return ; + } else { + return ; + } + } else { + return ; + } + }, + }; + } else { + proofResult = { + title: 'State', + dataIndex: ['record', 'state'], ellipsis: true, ...getColumnSearchProps( - ['record', 'thread_id'], + ['record', 'state'], searchState, setSearchState, ), - }, + }; + } + + return [ { title: 'Connection', dataIndex: ['connection', 'alias'], @@ -71,12 +104,13 @@ export const CompletedProofsList: React.FC = (props) => { setSearchState, ), }, + proofResult, { - title: 'State', - dataIndex: ['record', 'state'], + title: 'Thread', + dataIndex: ['record', 'thread_id'], ellipsis: true, ...getColumnSearchProps( - ['record', 'state'], + ['record', 'thread_id'], searchState, setSearchState, ), @@ -93,6 +127,7 @@ export const CompletedProofsList: React.FC = (props) => { }, { key: 'remove', + width: '10%', title: {'Remove'}, align: 'right', render(_, proofInfo) { @@ -128,6 +163,7 @@ export const CompletedProofsList: React.FC = (props) => { ); const columns = makeColumns({ + role: role, onRemove: removeButtonHandler, }); diff --git a/src/models/ACAPy/Connections.ts b/src/models/ACAPy/Connections.ts index 36f6d24..9f0ecfd 100644 --- a/src/models/ACAPy/Connections.ts +++ b/src/models/ACAPy/Connections.ts @@ -71,7 +71,7 @@ export async function createConnectionInvite( } catch (error) { throw await reportCloudAgentError( 'Failed to Create a connection invitation', - error, + error as Response, ); } } @@ -88,7 +88,10 @@ export async function acceptConnectionInvite( body: params.invitation, }); } catch (error) { - throw await reportCloudAgentError('Failed to Accept invitation', error); + throw await reportCloudAgentError( + 'Failed to Accept invitation', + error as Response, + ); } } @@ -101,7 +104,7 @@ export async function deleteConnection( } catch (error) { throw await reportCloudAgentError( `Failed to Delete connection: ${id}`, - error, + error as Response, ); } } @@ -118,7 +121,7 @@ export async function trustPingConnection( } catch (error) { throw await reportCloudAgentError( `Failed to Ping connection: ${id}`, - error, + error as Response, ); } } @@ -134,7 +137,7 @@ export async function fetchAllAgentConnectionDetails( } catch (error) { throw await reportCloudAgentError( 'Failed to Retrieve Connection List from Wallet', - error, + error as Response, ); } } diff --git a/src/models/ACAPy/CredentialDefinitions.ts b/src/models/ACAPy/CredentialDefinitions.ts index c2dd530..0d73a6b 100644 --- a/src/models/ACAPy/CredentialDefinitions.ts +++ b/src/models/ACAPy/CredentialDefinitions.ts @@ -22,6 +22,9 @@ export type CredentialDefinitionCreateParams = { tag: string; // Human readable, descriptive tag for credential definition schema: SchemaDefinitionId; // Identifier assigned at schema create time revocable: boolean; // Revocation supported for issued credentials + size?: number; // The revocation registry size. Only for revocable credential definitions. + // This effectively sets the number of credentials that can + // be issued for this definition. }; export async function fetchCredentialDefinitionIds( @@ -48,7 +51,7 @@ export async function fetchCredentialDefinitionIds( } catch (error) { throw await reportCloudAgentError( `Credential Definition not found for issuerDID: ${issuerDid} schemaId: ${schemaId}`, - error, + error as Response, ); } } @@ -64,12 +67,13 @@ export async function createCredentialDefinition( tag: params.tag, support_revocation: params.revocable, schema_id: params.schema, + revocation_registry_size: params.size, }, }); } catch (error) { throw await reportCloudAgentError( 'Failed to Create Credential Definition on Ledger', - error, + error as Response, ); } } @@ -87,7 +91,7 @@ export async function fetchCredentialDefinitionDetails( } catch (error) { throw await reportCloudAgentError( `Credential Definition ${id} NOT FOUND on Ledger`, - error, + error as Response, ); } } @@ -104,7 +108,7 @@ export async function fetchAllAgentCredentialDefinitionIds( } catch (error) { throw await reportCloudAgentError( 'Failed to Retrieve Credential Identifiers from Cloud Agent', - error, + error as Response, ); } } diff --git a/src/models/ACAPy/CredentialIssuance.ts b/src/models/ACAPy/CredentialIssuance.ts index 5ba348a..31b61ee 100644 --- a/src/models/ACAPy/CredentialIssuance.ts +++ b/src/models/ACAPy/CredentialIssuance.ts @@ -15,9 +15,14 @@ import { CloudAgentAPI } from '../../containers/App/AppContext'; import { ConnRecord, CredentialPreview, + CredentialRevokedCredentialIdGetRequest, + CredRevokedResult, + CredRevRecordResult, IndyCredInfo, IssueCredentialRecordsGetRoleEnum, IssueCredentialRecordsGetStateEnum, + RevocationCredentialRecordGetRequest, + RevokeRequest, V10CredentialExchange, } from '@sudoplatform-labs/sudo-di-cloud-agent'; import { reportCloudAgentError } from '../../utils/errorlog'; @@ -78,7 +83,7 @@ export async function deleteCredential( } catch (error) { throw await reportCloudAgentError( `Failed to Delete credential: ${id}`, - error, + error as Response, ); } } @@ -94,7 +99,7 @@ export async function deleteCredentialExchangeRecord( } catch (error) { throw await reportCloudAgentError( `Failed to Delete Credential Exchange Record: ${id}`, - error, + error as Response, ); } } @@ -117,7 +122,7 @@ export async function abortCredentialExchange( } catch (error) { throw await reportCloudAgentError( `Failed to Abort Credential Issue: ${id}`, - error, + error as Response, ); } } @@ -152,7 +157,7 @@ export async function proposeCredential( params.schema_name ?? params.connection_id }`, - error, + error as Response, ); } } @@ -170,7 +175,7 @@ export async function offerCredential( } catch (error) { throw await reportCloudAgentError( `Failed to Offer Credential : ${id}`, - error, + error as Response, ); } } @@ -188,7 +193,7 @@ export async function requestProposedCredential( } catch (error) { throw await reportCloudAgentError( `Failed to Request proposed Credential : ${id}`, - error, + error as Response, ); } } @@ -206,7 +211,7 @@ export async function issueCredential( } catch (error) { throw await reportCloudAgentError( `Failed to Issue requested Credential : ${id}`, - error, + error as Response, ); } } @@ -222,7 +227,55 @@ export async function storeCredential( } catch (error) { throw await reportCloudAgentError( `Failed to Store Credential : ${id}`, - error, + error as Response, + ); + } +} + +export async function revokeCredential( + agent: CloudAgentAPI, + params: RevokeRequest, +): Promise { + try { + await agent.revocations.revocationRevokePost({ body: params }); + } catch (error) { + throw await reportCloudAgentError( + `Failed to Revoke Credential : ${params.cred_ex_id}`, + error as Response, + ); + } +} + +export async function getIssuerCredentialRevocationStatus( + agent: CloudAgentAPI, + params: RevocationCredentialRecordGetRequest, +): Promise { + try { + const result = await agent.revocations.revocationCredentialRecordGet( + params, + ); + return result; + } catch (error) { + throw await reportCloudAgentError( + `Failed to retreive Revocation status : ${params.credExId}`, + error as Response, + ); + } +} + +export async function getHolderCredentialRevocationStatus( + agent: CloudAgentAPI, + params: CredentialRevokedCredentialIdGetRequest, +): Promise { + try { + const result = await agent.credentials.credentialRevokedCredentialIdGet( + params, + ); + return result; + } catch (error) { + throw await reportCloudAgentError( + `Failed to retreive Revocation status : ${params.credentialId}`, + error as Response, ); } } @@ -252,7 +305,7 @@ export async function fetchFilteredCredentialExchangeRecords( } catch (error) { throw await reportCloudAgentError( 'Failed to Retrieve Credential Exchange Records from Wallet', - error, + error as Response, ); } } @@ -268,7 +321,7 @@ export async function fetchAllAgentOwnedCredentialDetails( } catch (error) { throw await reportCloudAgentError( 'Failed to Retrieve Credential List from Wallet', - error, + error as Response, ); } } diff --git a/src/models/ACAPy/DecentralizedIdentifiers.ts b/src/models/ACAPy/DecentralizedIdentifiers.ts index ee4c034..695efc2 100644 --- a/src/models/ACAPy/DecentralizedIdentifiers.ts +++ b/src/models/ACAPy/DecentralizedIdentifiers.ts @@ -24,7 +24,7 @@ export async function createPrivateDID(agent: CloudAgentAPI): Promise { } catch (error) { throw await reportCloudAgentError( 'Failed to Create new Decentralized Identifier in Wallet', - error, + error as Response, ); } } @@ -38,7 +38,7 @@ export async function assignAgentsPublicDID( } catch (error) { throw await reportCloudAgentError( 'Failed to Assign new Public Decentralized Identifier for Wallet', - error, + error as Response, ); } } @@ -53,7 +53,7 @@ export async function writeDIDToLedger( } catch (error) { throw await reportCloudAgentError( 'Failed to Write Decentralized Identifier to Ledger', - error, + error as Response, ); } } @@ -65,7 +65,7 @@ export async function fetchAllAgentDIDs(agent: CloudAgentAPI): Promise { } catch (error) { throw await reportCloudAgentError( 'Failed to Retrieve Decentralized Identifiers from Wallet', - error, + error as Response, ); } } diff --git a/src/models/ACAPy/ProofPresentation.ts b/src/models/ACAPy/ProofPresentation.ts index ffde6cb..af95276 100644 --- a/src/models/ACAPy/ProofPresentation.ts +++ b/src/models/ACAPy/ProofPresentation.ts @@ -89,7 +89,7 @@ export async function fetchFilteredProofExchangeRecords( } catch (error) { throw await reportCloudAgentError( 'Failed to Retrieve Proof Exchange Records from Wallet', - error, + error as Response, ); } } @@ -117,7 +117,10 @@ export async function sendProofRequest( body: agentRequest, }); } catch (error) { - throw await reportCloudAgentError(`Failed to Send Proof Request`, error); + throw await reportCloudAgentError( + `Failed to Send Proof Request`, + error as Response, + ); } } @@ -132,7 +135,7 @@ export async function deleteProofExchange( } catch (error) { throw await reportCloudAgentError( `Failed to Delete Proof Exchange Record: ${id}`, - error, + error as Response, ); } } @@ -154,7 +157,7 @@ export async function fetchCredentialsMatchingProof( } catch (error) { throw await reportCloudAgentError( 'Failed to match Proof with credentials from Wallet', - error, + error as Response, ); } } @@ -174,7 +177,7 @@ export async function sendProofPresentation( } catch (error) { throw await reportCloudAgentError( 'Failed to send Proof Presentation to verifier', - error, + error as Response, ); } } @@ -192,7 +195,7 @@ export async function verifyProofPresentation( } catch (error) { throw await reportCloudAgentError( 'Failed to verify Proof Presentation', - error, + error as Response, ); } } diff --git a/src/models/ACAPy/SchemaDefinitions.ts b/src/models/ACAPy/SchemaDefinitions.ts index cf8d725..18d9143 100644 --- a/src/models/ACAPy/SchemaDefinitions.ts +++ b/src/models/ACAPy/SchemaDefinitions.ts @@ -43,7 +43,7 @@ export async function createSchemaDefinition( } catch (error) { throw await reportCloudAgentError( 'Failed to Create Schema on Ledger', - error, + error as Response, ); } } @@ -66,7 +66,7 @@ export async function fetchSchemaDefinitionIds( } catch (error) { throw await reportCloudAgentError( `Schema not found for creatorDID: ${did} schemaName: ${name} schemaVersion: ${version}`, - error, + error as Response, ); } } @@ -85,7 +85,7 @@ export async function fetchSchemaDefinitionDetails( } catch (error) { throw await reportCloudAgentError( `Schema ${id} NOT FOUND on Ledger`, - error, + error as Response, ); } } @@ -100,7 +100,7 @@ export async function fetchAllAgentSchemaDefinitionIds( } catch (error) { throw await reportCloudAgentError( 'Failed to Retrieve Defined Schema Identifiers from Cloud Agent', - error, + error as Response, ); } } diff --git a/src/models/ACAPy/TransactionAuthorAgreement.ts b/src/models/ACAPy/TransactionAuthorAgreement.ts index 436721e..a7d73b6 100644 --- a/src/models/ACAPy/TransactionAuthorAgreement.ts +++ b/src/models/ACAPy/TransactionAuthorAgreement.ts @@ -35,7 +35,7 @@ export async function fetchLedgerTaa(agent: CloudAgentAPI): Promise { } catch (error) { throw await reportCloudAgentError( `Current Transaction Author Agreement NOT RETURNED by Ledger`, - error, + error as Response, ); } } @@ -55,7 +55,7 @@ export async function acceptLedgerTaa( } catch (error) { throw await reportCloudAgentError( `Transaction Author Agreement NOT ACCEPTED by Ledger`, - error, + error as Response, ); } } diff --git a/src/pages/Connections/ConnectionsCard/ConnectionsCard.tsx b/src/pages/Connections/ConnectionsCard/ConnectionsCard.tsx index c353584..5740734 100644 --- a/src/pages/Connections/ConnectionsCard/ConnectionsCard.tsx +++ b/src/pages/Connections/ConnectionsCard/ConnectionsCard.tsx @@ -19,7 +19,6 @@ import { import { modalInfo, modalConfirm } from '../../../components/Form'; import { VStack, HStack } from '../../../components/layout-stacks'; import { AcceptInvitationForm } from './AcceptInvitationForm'; -import { useInterval } from '../../../utils/intervals'; import { ConnRecord, InvitationResult, @@ -94,17 +93,6 @@ export const ConnectionsCard: React.FC = () => { getConnectionsInfo(); }, [getConnectionsInfo, modalState]); - // Slow poll for any connection state changes since - // we don't have any ACA-py hooks implemented. - const [count, setCount] = useState(30); - useInterval(() => { - setCount(count - 2); - if (count <= 0) { - setCount(30); - getConnectionsInfo(); - } - }, 2000); - const modalCreateInvitationCancelHandler = useCallback(() => { setModalState('closed'); }, []); @@ -210,7 +198,6 @@ export const ConnectionsCard: React.FC = () => { } extra={ -
Refresh in {count.toString().padStart(2, '0')}