From 2d7fd92b95a6c82659e21d9bbc581ecf3815a6d5 Mon Sep 17 00:00:00 2001 From: pennhan-dex Date: Mon, 12 Jan 2026 14:50:17 +0800 Subject: [PATCH 1/8] implement w3c-sign --- package-lock.json | 31 +++++++------- src/commands/sign.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 src/commands/sign.ts diff --git a/package-lock.json b/package-lock.json index d525d67..8cbcca7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1451,6 +1451,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", @@ -1863,6 +1864,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", @@ -4562,8 +4564,7 @@ "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/signale": { "version": "1.4.7", @@ -4631,6 +4632,7 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -4978,6 +4980,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6392,6 +6395,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6456,6 +6460,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6712,6 +6717,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", @@ -7254,7 +7260,6 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8873,7 +8878,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -9079,6 +9083,7 @@ "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", "license": "MIT", + "peer": true, "engines": { "node": ">=14.16" }, @@ -10032,6 +10037,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10170,6 +10176,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11244,8 +11251,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", - "license": "WTFPL OR MIT", - "peer": true + "license": "WTFPL OR MIT" }, "node_modules/string-width": { "version": "4.2.3", @@ -11652,7 +11658,6 @@ "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", "license": "ISC", - "peer": true, "dependencies": { "chalk": "^4.1.0", "command-line-args": "^5.1.1", @@ -11771,6 +11776,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -11843,7 +11849,6 @@ "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", "license": "MIT", - "peer": true, "dependencies": { "@types/prettier": "^2.1.1", "debug": "^4.3.1", @@ -11868,7 +11873,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11880,7 +11884,6 @@ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11900,15 +11903,13 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/typechain/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11920,7 +11921,6 @@ "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12010,6 +12010,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12088,7 +12089,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -12780,6 +12780,7 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 8" } diff --git a/src/commands/sign.ts b/src/commands/sign.ts new file mode 100644 index 0000000..a49cd62 --- /dev/null +++ b/src/commands/sign.ts @@ -0,0 +1,96 @@ +import { input, select } from '@inquirer/prompts'; +import chalk from 'chalk'; +import { isDirectoryValid, readJsonFile } from '../utils'; +import { issuer, RawVerifiableCredential, signW3C } from '@trustvc/trustvc'; +import { writeFile } from '../utils/file-io'; +import { CryptoSuiteName } from '@trustvc/w3c-vc'; + +export const command = 'w3c-sign'; +export const describe = 'Sign a document using a key pair file'; + +export const handler = async () => { + try { + const answers = await promptForInputs(); + if (!answers) return; + + await sign(answers); + } catch (err: unknown) { + console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`)); + } +}; + +export const promptForInputs = async (): Promise => { + const pathToKeypairFile = await input({ + message: 'Please enter the path to your did key-pair JSON file:', + required: true, + validate: (value: string) => { + if (!value || value.trim() === '') { + return 'did key-pair JSON file path is required'; + } + return true; + }, + }); + + const pathToCredentialFile = await input({ + message: 'Pleaae enter the path to your credential JSON file:', + required: true, + validate: (value: string) => { + if (!value || value.trim() === '') { + return 'Credential JSON file path is required'; + } + return true; + }, + }); + + const encryptionAlgorithm = await select({ + message: 'Select the supported encryption algorithms', + choices: [ + { name: 'ECDSA-SD-2023', value: 'ecdsa-sd-2023', description: 'Sign credential using ECDSA-SD-2023 suite', }, + { name: 'BBS-2023', value: 'bbs-2023', description: 'Sign credential using BBS-2023 suite', }, + { name: 'ECDSA', value: 'ecdsa', description: 'Sign credential using ECDSA suite', }, + { name: 'BBS', value: 'bbs', description: 'Sign credential using BBS suite', }, + ], + default: 'ECDSA-SD-2023', + }); + + const pathToSignedVC = await input({ + message: 'Enter a file path to save the signed verifiable credential (default: current directory):', + default: '.', + required: true, + }); + + return { + pathToKeypairFile, + pathToCredentialFile, + encryptionAlgorithm, + pathToSignedVC, + }; +}; + +// === Implementation === + +type SignCommand = { + pathToKeypairFile: string; + pathToCredentialFile: string; + encryptionAlgorithm: string; + pathToSignedVC: string; +}; + +export const sign = async ({ + pathToKeypairFile, + pathToCredentialFile, + encryptionAlgorithm, + pathToSignedVC, +}: SignCommand): Promise => { + console.log(pathToCredentialFile); + const keypairData: typeof issuer.IssuedDIDOption = readJsonFile(pathToKeypairFile, 'key pair'); + const credential = readJsonFile(pathToCredentialFile, 'credential JSON') as RawVerifiableCredential; // This does not give informative error for wrongly formatted JSON + const algorithm = encryptionAlgorithm as CryptoSuiteName; + try { + const signedVC = await signW3C(credential, keypairData, algorithm); + console.log(signedVC.signed); + writeFile(pathToSignedVC, signedVC.signed); + } catch (err: unknown) { + console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`)); + } +}; \ No newline at end of file From 54fbfc3fce8d31c58c5b5e14ef85d2e9934ca19b Mon Sep 17 00:00:00 2001 From: pennhan-dex Date: Tue, 13 Jan 2026 09:40:58 +0800 Subject: [PATCH 2/8] add unit tests --- src/commands/sign.ts | 62 +++++---- src/types.ts | 9 +- tests/commands/sign.test.ts | 242 ++++++++++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 35 deletions(-) create mode 100644 tests/commands/sign.test.ts diff --git a/src/commands/sign.ts b/src/commands/sign.ts index a49cd62..f80d8c1 100644 --- a/src/commands/sign.ts +++ b/src/commands/sign.ts @@ -1,12 +1,11 @@ import { input, select } from '@inquirer/prompts'; import chalk from 'chalk'; -import { isDirectoryValid, readJsonFile } from '../utils'; +import { isDirectoryValid, readJsonFile, writeFile } from '../utils'; import { issuer, RawVerifiableCredential, signW3C } from '@trustvc/trustvc'; -import { writeFile } from '../utils/file-io'; -import { CryptoSuiteName } from '@trustvc/w3c-vc'; +import { SignInput } from '../types'; export const command = 'w3c-sign'; -export const describe = 'Sign a document using a key pair file'; +export const describe = 'Sign a verifiable credential using a did key-pair file'; export const handler = async () => { try { @@ -19,7 +18,7 @@ export const handler = async () => { } }; -export const promptForInputs = async (): Promise => { +export const promptForInputs = async (): Promise => { const pathToKeypairFile = await input({ message: 'Please enter the path to your did key-pair JSON file:', required: true, @@ -31,6 +30,8 @@ export const promptForInputs = async (): Promise => { }, }); + const keyPairData: typeof issuer.IssuedDIDOption = readJsonFile(pathToKeypairFile, 'key pair'); + const pathToCredentialFile = await input({ message: 'Pleaae enter the path to your credential JSON file:', required: true, @@ -42,55 +43,48 @@ export const promptForInputs = async (): Promise => { }, }); + const credential: RawVerifiableCredential = readJsonFile(pathToCredentialFile, 'credential JSON'); + const encryptionAlgorithm = await select({ - message: 'Select the supported encryption algorithms', + message: 'Select the encryption algorithm used to generate the key pair:', choices: [ { name: 'ECDSA-SD-2023', value: 'ecdsa-sd-2023', description: 'Sign credential using ECDSA-SD-2023 suite', }, { name: 'BBS-2023', value: 'bbs-2023', description: 'Sign credential using BBS-2023 suite', }, - { name: 'ECDSA', value: 'ecdsa', description: 'Sign credential using ECDSA suite', }, - { name: 'BBS', value: 'bbs', description: 'Sign credential using BBS suite', }, ], default: 'ECDSA-SD-2023', }); const pathToSignedVC = await input({ - message: 'Enter a file path to save the signed verifiable credential (default: current directory):', + message: 'Enter a directory to save the signed verifiable credential (optional):', default: '.', - required: true, + required: false, }); + if (!isDirectoryValid(pathToSignedVC)) throw new Error('Output path is not valid'); + return { - pathToKeypairFile, - pathToCredentialFile, + keyPairData, + credential, encryptionAlgorithm, pathToSignedVC, }; }; -// === Implementation === - -type SignCommand = { - pathToKeypairFile: string; - pathToCredentialFile: string; - encryptionAlgorithm: string; - pathToSignedVC: string; -}; - export const sign = async ({ - pathToKeypairFile, - pathToCredentialFile, + keyPairData, + credential, encryptionAlgorithm, pathToSignedVC, -}: SignCommand): Promise => { - console.log(pathToCredentialFile); - const keypairData: typeof issuer.IssuedDIDOption = readJsonFile(pathToKeypairFile, 'key pair'); - const credential = readJsonFile(pathToCredentialFile, 'credential JSON') as RawVerifiableCredential; // This does not give informative error for wrongly formatted JSON - const algorithm = encryptionAlgorithm as CryptoSuiteName; - try { - const signedVC = await signW3C(credential, keypairData, algorithm); - console.log(signedVC.signed); - writeFile(pathToSignedVC, signedVC.signed); - } catch (err: unknown) { - console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`)); +}: SignInput): Promise => { + const signedVC = await signW3C(credential, keyPairData, encryptionAlgorithm); + if (signedVC?.signed) { + const signedVCPath = `${pathToSignedVC}/signed_vc.json`; + writeFile(signedVCPath, signedVC.signed); + console.log(''); // blank line for spacing + // signale.success(chalk.green('Signed verifiable credential saved to: ' + pathToSignedVC)); + } + else { + console.log(''); // blank line for spacing + // signale.error(chalk.red(signedVC.error)); } }; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index b883b48..dc1f283 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,11 @@ -import { issuer } from '@trustvc/trustvc'; +import { credentialStatus, issuer, RawVerifiableCredential } from '@trustvc/trustvc'; + +export type SignInput = { + keyPairData: typeof issuer.IssuedDIDOption ; + credential: RawVerifiableCredential; + encryptionAlgorithm: typeof credentialStatus.cryptoSuiteName; + pathToSignedVC: string; +} export type DidInput = { keyPairPath: string; diff --git a/tests/commands/sign.test.ts b/tests/commands/sign.test.ts new file mode 100644 index 0000000..7b0f3fd --- /dev/null +++ b/tests/commands/sign.test.ts @@ -0,0 +1,242 @@ +import * as prompts from '@inquirer/prompts'; +import { issuer } from '@trustvc/trustvc'; +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { promptForInputs, sign } from '../../src/commands/sign'; +import { SignInput } from '../../src/types'; + + +vi.mock('@inquirer/prompts'); + +vi.mock('../../src/utils', async () => { + const actual = await vi.importActual('../../src/utils'); + return { + ...actual, + readJsonFile: vi.fn(), + isDirectoryValid: vi.fn(), + writeFile: vi.fn(), + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + signW3C: vi.fn(), + }; +}); + + +const mockPromptFlow = ( + algorithm: 'ecdsa-sd-2023' | 'bbs-2023' = 'ecdsa-sd-2023', +) => { + (prompts.input as any) + .mockResolvedValueOnce('./did-keypair.json') + .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('.'); + + (prompts.select as any).mockResolvedValueOnce(algorithm); +}; + +const mockUtilsHappyPath = async ( + keyPairData = { domain: 'https://example.com' }, + credential = { id: 'urn:uuid:123' }, +) => { + const utils = await import('../../src/utils'); + + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce(keyPairData) + .mockReturnValueOnce(credential); + + (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); + + return utils; +}; + +const mockSignSuccess = async (signed: any) => { + const trustvc = await import('@trustvc/trustvc'); + (trustvc.signW3C as MockedFunction).mockResolvedValue({ signed }); +}; + + +describe('w3c-sign', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + describe('promptForInputs', () => { + it('returns parsed inputs when algorithm is ecdsa-sd-2023', async () => { + mockPromptFlow('ecdsa-sd-2023'); + await mockUtilsHappyPath(); + + const result = await promptForInputs(); + + expect(result).toStrictEqual({ + keyPairData: { domain: 'https://example.com' }, + credential: { id: 'urn:uuid:123' }, + encryptionAlgorithm: 'ecdsa-sd-2023', + pathToSignedVC: '.', + }); + }); + + it('returns parsed inputs when algorithm is bbs-2023', async () => { + mockPromptFlow('bbs-2023'); + await mockUtilsHappyPath(); + + const result = await promptForInputs(); + + expect(result).toStrictEqual({ + keyPairData: { domain: 'https://example.com' }, + credential: { id: 'urn:uuid:123' }, + encryptionAlgorithm: 'bbs-2023', + pathToSignedVC: '.', + }); + }); + + it('provides required validation rules for inputs', async () => { + mockPromptFlow('bbs-2023'); + await mockUtilsHappyPath(); + + await promptForInputs(); + + const [keypairArgs, credentialArgs, signedVcArgs] = + (prompts.input as any).mock.calls.map((c: any[]) => c[0]); + + expect(keypairArgs.required).toBe(true); + expect(keypairArgs.validate('')).toBe('did key-pair JSON file path is required'); + expect(keypairArgs.validate(' ')).toBe('did key-pair JSON file path is required'); + expect(keypairArgs.validate('./did-keypair.json')).toBe(true); + + expect(credentialArgs.required).toBe(true); + expect(credentialArgs.validate('')).toBe('Credential JSON file path is required'); + expect(credentialArgs.validate(' ')).toBe('Credential JSON file path is required'); + expect(credentialArgs.validate('./credential.json')).toBe(true); + + expect(signedVcArgs.required).toBe(false); + expect(signedVcArgs.default).toBe('.'); + }); + + it('prompts for encryption algorithm with supported choices', async () => { + mockPromptFlow(); + await mockUtilsHappyPath(); + + await promptForInputs(); + + const selectArgs = (prompts.select as any).mock.calls[0][0]; + + expect(selectArgs.message).toContain('Select the encryption algorithm'); + expect(selectArgs.choices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ value: 'ecdsa-sd-2023' }), + expect.objectContaining({ value: 'bbs-2023' }), + ]), + ); + }); + + it('throws when given an invalid did key-pair file path (readJsonFile fails)', async () => { + mockPromptFlow('ecdsa-sd-2023'); + const utils = await import('../../src/utils'); + + (utils.readJsonFile as MockedFunction).mockImplementation(() => { + throw new Error('Invalid key pair file path: ./did-keypair.json'); + }); + + await expect(promptForInputs()).rejects.toThrow( + 'Invalid key pair file path: ./did-keypair.json', + ); + }); + + it('throws when given an invalid credential file path (readJsonFile fails)', async () => { + mockPromptFlow('ecdsa-sd-2023'); + const utils = await import('../../src/utils'); + + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockImplementation(() => { + throw new Error('Invalid credential JSON file path: ./credential.json'); + }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); + + await expect(promptForInputs()).rejects.toThrow( + 'Invalid credential JSON file path: ./credential.json', + ); + }); + + it('throws when output path is not a valid directory', async () => { + mockPromptFlow('ecdsa-sd-2023'); + const utils = await mockUtilsHappyPath(); + (utils.isDirectoryValid as MockedFunction).mockReturnValue(false); + + await expect(promptForInputs()).rejects.toThrow('Output path is not valid'); + }); + }); + + + describe('sign', () => { + let writeFileMock: MockedFunction; + let signW3CMock: MockedFunction; + + const input: SignInput = { + keyPairData: { domain: 'https://example.com' } as typeof issuer.IssuedDIDOption, + credential: { id: 'urn:uuid:123' }, + encryptionAlgorithm: 'ecdsa-sd-2023', + pathToSignedVC: '.', + }; + + beforeEach(async () => { + const utils = await import('../../src/utils'); + writeFileMock = utils.writeFile as MockedFunction; + + const trustvc = await import('@trustvc/trustvc'); + signW3CMock = trustvc.signW3C as MockedFunction; + }); + + it('signs with ecdsa-sd-2023 and writes to default output path', async () => { + signW3CMock.mockResolvedValue({ signed: { proof: 'ok' } }); + + await sign({ ...input, encryptionAlgorithm: 'ecdsa-sd-2023', pathToSignedVC: '.' }); + + expect(signW3CMock).toHaveBeenCalledWith( + input.credential, + input.keyPairData, + 'ecdsa-sd-2023', + ); + expect(writeFileMock).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }); + }); + + it('signs with bbs-2023 and writes to a custom output directory', async () => { + signW3CMock.mockResolvedValue({ signed: { proof: 'ok' } }); + + await sign({ + ...input, + encryptionAlgorithm: 'bbs-2023', + pathToSignedVC: './out', + }); + + expect(signW3CMock).toHaveBeenCalledWith( + input.credential, + input.keyPairData, + 'bbs-2023', + ); + expect(writeFileMock).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }); + }); + + it('does not write file when signing fails', async () => { + signW3CMock.mockResolvedValue({ error: 'Failed to sign' }); + + await sign(input); + + expect(writeFileMock).not.toHaveBeenCalled(); + }); + + it('throws when writing signed VC fails', async () => { + writeFileMock.mockImplementation(() => { + throw new Error('Unexpected error while writing'); + }); + + signW3CMock.mockResolvedValue({ signed: {} }); + + await expect(sign(input)).rejects.toThrow('Unexpected error while writing'); + }); + }); +}); From acb45befcf19dfbefd63f4c90692b8bda07c91c9 Mon Sep 17 00:00:00 2001 From: pennhan-dex Date: Tue, 13 Jan 2026 10:08:19 +0800 Subject: [PATCH 3/8] update unit tests --- src/commands/{ => w3c}/sign.ts | 14 ++- tests/commands/{ => w3c}/sign.test.ts | 148 +++++++++++++++++--------- tests/main.test.ts | 124 +++++++++++++++++++++ 3 files changed, 226 insertions(+), 60 deletions(-) rename src/commands/{ => w3c}/sign.ts (84%) rename tests/commands/{ => w3c}/sign.test.ts (58%) diff --git a/src/commands/sign.ts b/src/commands/w3c/sign.ts similarity index 84% rename from src/commands/sign.ts rename to src/commands/w3c/sign.ts index f80d8c1..854faf2 100644 --- a/src/commands/sign.ts +++ b/src/commands/w3c/sign.ts @@ -1,8 +1,8 @@ import { input, select } from '@inquirer/prompts'; -import chalk from 'chalk'; -import { isDirectoryValid, readJsonFile, writeFile } from '../utils'; +import { isDirectoryValid, readJsonFile, writeFile } from '../../utils'; import { issuer, RawVerifiableCredential, signW3C } from '@trustvc/trustvc'; -import { SignInput } from '../types'; +import { SignInput } from '../../types'; +import signale from 'signale'; export const command = 'w3c-sign'; export const describe = 'Sign a verifiable credential using a did key-pair file'; @@ -14,7 +14,7 @@ export const handler = async () => { await sign(answers); } catch (err: unknown) { - console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`)); + signale.error(`${err instanceof Error ? err.message : String(err)}`); } }; @@ -80,11 +80,9 @@ export const sign = async ({ if (signedVC?.signed) { const signedVCPath = `${pathToSignedVC}/signed_vc.json`; writeFile(signedVCPath, signedVC.signed); - console.log(''); // blank line for spacing - // signale.success(chalk.green('Signed verifiable credential saved to: ' + pathToSignedVC)); + signale.success('\nSigned verifiable credential saved to: ' + pathToSignedVC); } else { - console.log(''); // blank line for spacing - // signale.error(chalk.red(signedVC.error)); + signale.error('\n' + signedVC.error); } }; \ No newline at end of file diff --git a/tests/commands/sign.test.ts b/tests/commands/w3c/sign.test.ts similarity index 58% rename from tests/commands/sign.test.ts rename to tests/commands/w3c/sign.test.ts index 7b0f3fd..46c6291 100644 --- a/tests/commands/sign.test.ts +++ b/tests/commands/w3c/sign.test.ts @@ -1,14 +1,29 @@ import * as prompts from '@inquirer/prompts'; import { issuer } from '@trustvc/trustvc'; import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; -import { promptForInputs, sign } from '../../src/commands/sign'; -import { SignInput } from '../../src/types'; +import { promptForInputs, sign } from '../../../src/commands/w3c/sign'; +import { SignInput } from '../../../src/types'; vi.mock('@inquirer/prompts'); -vi.mock('../../src/utils', async () => { - const actual = await vi.importActual('../../src/utils'); +vi.mock('signale', () => ({ + default: { + success: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + }, + Signale: vi.fn().mockImplementation(() => ({ + await: vi.fn(), + success: vi.fn(), + })), +})); + +vi.mock('../../../src/utils', async () => { + const actual = await vi.importActual( + '../../../src/utils', + ); return { ...actual, readJsonFile: vi.fn(), @@ -25,39 +40,6 @@ vi.mock('@trustvc/trustvc', async () => { }; }); - -const mockPromptFlow = ( - algorithm: 'ecdsa-sd-2023' | 'bbs-2023' = 'ecdsa-sd-2023', -) => { - (prompts.input as any) - .mockResolvedValueOnce('./did-keypair.json') - .mockResolvedValueOnce('./credential.json') - .mockResolvedValueOnce('.'); - - (prompts.select as any).mockResolvedValueOnce(algorithm); -}; - -const mockUtilsHappyPath = async ( - keyPairData = { domain: 'https://example.com' }, - credential = { id: 'urn:uuid:123' }, -) => { - const utils = await import('../../src/utils'); - - (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce(keyPairData) - .mockReturnValueOnce(credential); - - (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); - - return utils; -}; - -const mockSignSuccess = async (signed: any) => { - const trustvc = await import('@trustvc/trustvc'); - (trustvc.signW3C as MockedFunction).mockResolvedValue({ signed }); -}; - - describe('w3c-sign', () => { beforeEach(() => { vi.clearAllMocks(); @@ -66,8 +48,17 @@ describe('w3c-sign', () => { describe('promptForInputs', () => { it('returns parsed inputs when algorithm is ecdsa-sd-2023', async () => { - mockPromptFlow('ecdsa-sd-2023'); - await mockUtilsHappyPath(); + (prompts.input as any) + .mockResolvedValueOnce('./did-keypair.json') + .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('.'); + (prompts.select as any).mockResolvedValueOnce('ecdsa-sd-2023'); + + const utils = await import('../../../src/utils'); + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); const result = await promptForInputs(); @@ -80,8 +71,17 @@ describe('w3c-sign', () => { }); it('returns parsed inputs when algorithm is bbs-2023', async () => { - mockPromptFlow('bbs-2023'); - await mockUtilsHappyPath(); + (prompts.input as any) + .mockResolvedValueOnce('./did-keypair.json') + .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('.'); + (prompts.select as any).mockResolvedValueOnce('bbs-2023'); + + const utils = await import('../../../src/utils'); + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); const result = await promptForInputs(); @@ -94,8 +94,17 @@ describe('w3c-sign', () => { }); it('provides required validation rules for inputs', async () => { - mockPromptFlow('bbs-2023'); - await mockUtilsHappyPath(); + (prompts.input as any) + .mockResolvedValueOnce('./did-keypair.json') + .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('.'); + (prompts.select as any).mockResolvedValueOnce('bbs-2023'); + + const utils = await import('../../../src/utils'); + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); await promptForInputs(); @@ -117,8 +126,17 @@ describe('w3c-sign', () => { }); it('prompts for encryption algorithm with supported choices', async () => { - mockPromptFlow(); - await mockUtilsHappyPath(); + (prompts.input as any) + .mockResolvedValueOnce('./did-keypair.json') + .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('.'); + (prompts.select as any).mockResolvedValueOnce('ecdsa-sd-2023'); + + const utils = await import('../../../src/utils'); + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); await promptForInputs(); @@ -134,8 +152,8 @@ describe('w3c-sign', () => { }); it('throws when given an invalid did key-pair file path (readJsonFile fails)', async () => { - mockPromptFlow('ecdsa-sd-2023'); - const utils = await import('../../src/utils'); + (prompts.input as any).mockResolvedValueOnce('./did-keypair.json'); + const utils = await import('../../../src/utils'); (utils.readJsonFile as MockedFunction).mockImplementation(() => { throw new Error('Invalid key pair file path: ./did-keypair.json'); @@ -147,8 +165,10 @@ describe('w3c-sign', () => { }); it('throws when given an invalid credential file path (readJsonFile fails)', async () => { - mockPromptFlow('ecdsa-sd-2023'); - const utils = await import('../../src/utils'); + (prompts.input as any) + .mockResolvedValueOnce('./did-keypair.json') + .mockResolvedValueOnce('./credential.json'); + const utils = await import('../../../src/utils'); (utils.readJsonFile as MockedFunction) .mockReturnValueOnce({ domain: 'https://example.com' }) @@ -163,8 +183,16 @@ describe('w3c-sign', () => { }); it('throws when output path is not a valid directory', async () => { - mockPromptFlow('ecdsa-sd-2023'); - const utils = await mockUtilsHappyPath(); + (prompts.input as any) + .mockResolvedValueOnce('./did-keypair.json') + .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('./invalid-dir'); + (prompts.select as any).mockResolvedValueOnce('ecdsa-sd-2023'); + + const utils = await import('../../../src/utils'); + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue(false); await expect(promptForInputs()).rejects.toThrow('Output path is not valid'); @@ -175,6 +203,8 @@ describe('w3c-sign', () => { describe('sign', () => { let writeFileMock: MockedFunction; let signW3CMock: MockedFunction; + let signaleSuccessMock: MockedFunction; + let signaleErrorMock: MockedFunction; const input: SignInput = { keyPairData: { domain: 'https://example.com' } as typeof issuer.IssuedDIDOption, @@ -184,11 +214,15 @@ describe('w3c-sign', () => { }; beforeEach(async () => { - const utils = await import('../../src/utils'); + const utils = await import('../../../src/utils'); writeFileMock = utils.writeFile as MockedFunction; const trustvc = await import('@trustvc/trustvc'); signW3CMock = trustvc.signW3C as MockedFunction; + + const signale = await import('signale'); + signaleSuccessMock = (signale.default as any).success; + signaleErrorMock = (signale.default as any).error; }); it('signs with ecdsa-sd-2023 and writes to default output path', async () => { @@ -202,6 +236,10 @@ describe('w3c-sign', () => { 'ecdsa-sd-2023', ); expect(writeFileMock).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }); + expect(signaleSuccessMock).toHaveBeenCalledWith( + expect.stringContaining('Signed verifiable credential saved to: .'), + ); + expect(signaleErrorMock).not.toHaveBeenCalled(); }); it('signs with bbs-2023 and writes to a custom output directory', async () => { @@ -219,6 +257,10 @@ describe('w3c-sign', () => { 'bbs-2023', ); expect(writeFileMock).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }); + expect(signaleSuccessMock).toHaveBeenCalledWith( + expect.stringContaining('Signed verifiable credential saved to: ./out'), + ); + expect(signaleErrorMock).not.toHaveBeenCalled(); }); it('does not write file when signing fails', async () => { @@ -227,6 +269,8 @@ describe('w3c-sign', () => { await sign(input); expect(writeFileMock).not.toHaveBeenCalled(); + expect(signaleSuccessMock).not.toHaveBeenCalled(); + expect(signaleErrorMock).toHaveBeenCalledWith(expect.stringContaining('Failed to sign')); }); it('throws when writing signed VC fails', async () => { diff --git a/tests/main.test.ts b/tests/main.test.ts index 024db00..32ee95f 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -3,6 +3,7 @@ import signale from 'signale'; import { beforeEach, describe, it, MockedFunction, vi } from 'vitest'; import { handler as didHandler } from '../src/commands/w3c/did'; import { handler as keyPairHandler } from '../src/commands/w3c/key-pair'; +import { handler as signHandler } from '../src/commands/w3c/sign'; import * as utils from '../src/utils'; vi.mock('signale', () => ({ @@ -45,6 +46,7 @@ vi.mock('@trustvc/trustvc', async () => { const original = await vi.importActual('@trustvc/trustvc'); return { ...original, + signW3C: vi.fn(), issuer: { ...original.issuer, issueDID: vi.fn(), @@ -371,4 +373,126 @@ describe('trustvc-cli', () => { expect(signaleErrorSpy).toHaveBeenCalledWith('Error generating keypair'); }); }); + + describe('trustvc w3c-sign command', () => { + let signW3CSpy: MockedFunction; + let writeFileSpy: MockedFunction; + let signaleErrorSpy: MockedFunction; + let signaleSuccessSpy: MockedFunction; + + beforeEach(async () => { + vi.clearAllMocks(); + vi.resetAllMocks(); + vi.restoreAllMocks(); + + const trustvc = await import('@trustvc/trustvc'); + signW3CSpy = trustvc.signW3C as MockedFunction; + writeFileSpy = vi.spyOn(utils, 'writeFile') as MockedFunction; + signaleErrorSpy = signale.error as MockedFunction; + signaleSuccessSpy = signale.success as MockedFunction; + }); + + it('should sign a credential using ecdsa-sd-2023 and write signed VC', async ({ expect }) => { + const input = { + keyPairPath: './did-keypair.json', + credentialPath: './credential.json', + encryptionAlgorithm: 'ecdsa-sd-2023', + outputPath: '.', + }; + + (prompts.input as any) + .mockResolvedValueOnce(input.keyPairPath) + .mockResolvedValueOnce(input.credentialPath) + .mockResolvedValueOnce(input.outputPath); + (prompts.select as any).mockResolvedValueOnce(input.encryptionAlgorithm); + + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue( + true, + ); + + signW3CSpy.mockResolvedValue({ signed: { proof: 'ok' } }); + + await signHandler(); + + expect(signW3CSpy).toHaveBeenCalledWith( + { id: 'urn:uuid:123' }, + { domain: 'https://example.com' }, + 'ecdsa-sd-2023', + ); + expect(writeFileSpy).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }); + expect(signaleSuccessSpy).toHaveBeenCalledWith( + expect.stringContaining('Signed verifiable credential saved to: .'), + ); + expect(signaleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should sign a credential using bbs-2023 and write signed VC to custom directory', async ({ expect }) => { + const input = { + keyPairPath: './did-keypair.json', + credentialPath: './credential.json', + encryptionAlgorithm: 'bbs-2023', + outputPath: './out', + }; + + (prompts.input as any) + .mockResolvedValueOnce(input.keyPairPath) + .mockResolvedValueOnce(input.credentialPath) + .mockResolvedValueOnce(input.outputPath); + (prompts.select as any).mockResolvedValueOnce(input.encryptionAlgorithm); + + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue( + true, + ); + + signW3CSpy.mockResolvedValue({ signed: { proof: 'ok' } }); + + await signHandler(); + + expect(signW3CSpy).toHaveBeenCalledWith( + { id: 'urn:uuid:123' }, + { domain: 'https://example.com' }, + 'bbs-2023', + ); + expect(writeFileSpy).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }); + expect(signaleSuccessSpy).toHaveBeenCalledWith( + expect.stringContaining('Signed verifiable credential saved to: ./out'), + ); + expect(signaleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle invalid output directory path', async ({ expect }) => { + const input = { + keyPairPath: './did-keypair.json', + credentialPath: './credential.json', + encryptionAlgorithm: 'ecdsa-sd-2023', + outputPath: './invalid-dir', + }; + + (prompts.input as any) + .mockResolvedValueOnce(input.keyPairPath) + .mockResolvedValueOnce(input.credentialPath) + .mockResolvedValueOnce(input.outputPath); + (prompts.select as any).mockResolvedValueOnce(input.encryptionAlgorithm); + + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue( + false, + ); + + await signHandler(); + + expect(signW3CSpy).not.toHaveBeenCalled(); + expect(writeFileSpy).not.toHaveBeenCalled(); + expect(signaleSuccessSpy).not.toHaveBeenCalled(); + expect(signaleErrorSpy).toHaveBeenCalledWith('Output path is not valid'); + }); + }); }); From d56988ae4b0f441548b7efa1a55a200c5c72a8b0 Mon Sep 17 00:00:00 2001 From: pennhan-dex Date: Tue, 13 Jan 2026 10:27:50 +0800 Subject: [PATCH 4/8] update unit tests --- src/commands/w3c/sign.ts | 6 ++--- tests/commands/w3c/sign.test.ts | 35 ++++++++++++------------- tests/main.test.ts | 45 ++++++++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/commands/w3c/sign.ts b/src/commands/w3c/sign.ts index 854faf2..bea335e 100644 --- a/src/commands/w3c/sign.ts +++ b/src/commands/w3c/sign.ts @@ -80,9 +80,7 @@ export const sign = async ({ if (signedVC?.signed) { const signedVCPath = `${pathToSignedVC}/signed_vc.json`; writeFile(signedVCPath, signedVC.signed); - signale.success('\nSigned verifiable credential saved to: ' + pathToSignedVC); - } - else { - signale.error('\n' + signedVC.error); + } else { + signale.error(signedVC.error); } }; \ No newline at end of file diff --git a/tests/commands/w3c/sign.test.ts b/tests/commands/w3c/sign.test.ts index 46c6291..5312b3d 100644 --- a/tests/commands/w3c/sign.test.ts +++ b/tests/commands/w3c/sign.test.ts @@ -47,7 +47,7 @@ describe('w3c-sign', () => { }); describe('promptForInputs', () => { - it('returns parsed inputs when algorithm is ecdsa-sd-2023', async () => { + it('should return parsed inputs when algorithm is ecdsa-sd-2023', async () => { (prompts.input as any) .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') @@ -70,7 +70,7 @@ describe('w3c-sign', () => { }); }); - it('returns parsed inputs when algorithm is bbs-2023', async () => { + it('should return parsed inputs when algorithm is bbs-2023', async () => { (prompts.input as any) .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') @@ -93,7 +93,7 @@ describe('w3c-sign', () => { }); }); - it('provides required validation rules for inputs', async () => { + it('should abide by validation rules for path inputs', async () => { (prompts.input as any) .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') @@ -125,7 +125,7 @@ describe('w3c-sign', () => { expect(signedVcArgs.default).toBe('.'); }); - it('prompts for encryption algorithm with supported choices', async () => { + it('should prompt for encryption algorithm with supported choices', async () => { (prompts.input as any) .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') @@ -151,7 +151,7 @@ describe('w3c-sign', () => { ); }); - it('throws when given an invalid did key-pair file path (readJsonFile fails)', async () => { + it('should throw error when given an invalid did key-pair file path (readJsonFile fails)', async () => { (prompts.input as any).mockResolvedValueOnce('./did-keypair.json'); const utils = await import('../../../src/utils'); @@ -164,7 +164,7 @@ describe('w3c-sign', () => { ); }); - it('throws when given an invalid credential file path (readJsonFile fails)', async () => { + it('should throw error when given an invalid credential file path (readJsonFile fails)', async () => { (prompts.input as any) .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json'); @@ -182,7 +182,7 @@ describe('w3c-sign', () => { ); }); - it('throws when output path is not a valid directory', async () => { + it('should throw error when output path is not a valid directory', async () => { (prompts.input as any) .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') @@ -223,9 +223,14 @@ describe('w3c-sign', () => { const signale = await import('signale'); signaleSuccessMock = (signale.default as any).success; signaleErrorMock = (signale.default as any).error; + + writeFileMock.mockImplementation((...args: unknown[]) => { + const filePath = args[0] as string; + signaleSuccessMock(`Saved: ${filePath}`); + }); }); - it('signs with ecdsa-sd-2023 and writes to default output path', async () => { + it('should sign with ecdsa-sd-2023 and writes to default output path', async () => { signW3CMock.mockResolvedValue({ signed: { proof: 'ok' } }); await sign({ ...input, encryptionAlgorithm: 'ecdsa-sd-2023', pathToSignedVC: '.' }); @@ -236,13 +241,11 @@ describe('w3c-sign', () => { 'ecdsa-sd-2023', ); expect(writeFileMock).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }); - expect(signaleSuccessMock).toHaveBeenCalledWith( - expect.stringContaining('Signed verifiable credential saved to: .'), - ); + expect(signaleSuccessMock).toHaveBeenCalledWith('Saved: ./signed_vc.json'); expect(signaleErrorMock).not.toHaveBeenCalled(); }); - it('signs with bbs-2023 and writes to a custom output directory', async () => { + it('should sign with bbs-2023 and writes to a custom output directory', async () => { signW3CMock.mockResolvedValue({ signed: { proof: 'ok' } }); await sign({ @@ -257,13 +260,11 @@ describe('w3c-sign', () => { 'bbs-2023', ); expect(writeFileMock).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }); - expect(signaleSuccessMock).toHaveBeenCalledWith( - expect.stringContaining('Signed verifiable credential saved to: ./out'), - ); + expect(signaleSuccessMock).toHaveBeenCalledWith('Saved: ./out/signed_vc.json'); expect(signaleErrorMock).not.toHaveBeenCalled(); }); - it('does not write file when signing fails', async () => { + it('should not write file when signing fails', async () => { signW3CMock.mockResolvedValue({ error: 'Failed to sign' }); await sign(input); @@ -273,7 +274,7 @@ describe('w3c-sign', () => { expect(signaleErrorMock).toHaveBeenCalledWith(expect.stringContaining('Failed to sign')); }); - it('throws when writing signed VC fails', async () => { + it('should throw error when writing signed VC to disk fails', async () => { writeFileMock.mockImplementation(() => { throw new Error('Unexpected error while writing'); }); diff --git a/tests/main.test.ts b/tests/main.test.ts index 32ee95f..62d0b78 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -390,6 +390,10 @@ describe('trustvc-cli', () => { writeFileSpy = vi.spyOn(utils, 'writeFile') as MockedFunction; signaleErrorSpy = signale.error as MockedFunction; signaleSuccessSpy = signale.success as MockedFunction; + + writeFileSpy.mockImplementation(((filePath: string) => { + signaleSuccessSpy(`Saved: ${filePath}`); + }) as any); }); it('should sign a credential using ecdsa-sd-2023 and write signed VC', async ({ expect }) => { @@ -423,9 +427,7 @@ describe('trustvc-cli', () => { 'ecdsa-sd-2023', ); expect(writeFileSpy).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }); - expect(signaleSuccessSpy).toHaveBeenCalledWith( - expect.stringContaining('Signed verifiable credential saved to: .'), - ); + expect(signaleSuccessSpy).toHaveBeenCalledWith('Saved: ./signed_vc.json'); expect(signaleErrorSpy).not.toHaveBeenCalled(); }); @@ -460,9 +462,7 @@ describe('trustvc-cli', () => { 'bbs-2023', ); expect(writeFileSpy).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }); - expect(signaleSuccessSpy).toHaveBeenCalledWith( - expect.stringContaining('Signed verifiable credential saved to: ./out'), - ); + expect(signaleSuccessSpy).toHaveBeenCalledWith('Saved: ./out/signed_vc.json'); expect(signaleErrorSpy).not.toHaveBeenCalled(); }); @@ -494,5 +494,38 @@ describe('trustvc-cli', () => { expect(signaleSuccessSpy).not.toHaveBeenCalled(); expect(signaleErrorSpy).toHaveBeenCalledWith('Output path is not valid'); }); + + it('should handle write file error', async ({ expect }) => { + const input = { + keyPairPath: './did-keypair.json', + credentialPath: './credential.json', + encryptionAlgorithm: 'ecdsa-sd-2023', + outputPath: '.', + }; + + (prompts.input as any) + .mockResolvedValueOnce(input.keyPairPath) + .mockResolvedValueOnce(input.credentialPath) + .mockResolvedValueOnce(input.outputPath); + (prompts.select as any).mockResolvedValueOnce(input.encryptionAlgorithm); + + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ domain: 'https://example.com' }) + .mockReturnValueOnce({ id: 'urn:uuid:123' }); + (utils.isDirectoryValid as MockedFunction).mockReturnValue( + true, + ); + + signW3CSpy.mockResolvedValue({ signed: { proof: 'ok' } }); + writeFileSpy.mockImplementationOnce(() => { + throw new Error('Unable to write file to ./signed_vc.json'); + }); + + await signHandler(); + + expect(signW3CSpy).toHaveBeenCalled(); + expect(signaleSuccessSpy).not.toHaveBeenCalled(); + expect(signaleErrorSpy).toHaveBeenCalledWith('Unable to write file to ./signed_vc.json'); + }); }); }); From 7bfa0a6fa30a8359423ac1c99912ecbd29f997de Mon Sep 17 00:00:00 2001 From: pennhan-dex Date: Tue, 13 Jan 2026 10:47:11 +0800 Subject: [PATCH 5/8] revert package.json --- package-lock.json | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 392e9b4..8868ecd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1366,6 +1366,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", @@ -1759,6 +1760,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", @@ -4299,8 +4301,7 @@ "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "peer": true + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" }, "node_modules/@types/signale": { "version": "1.4.7", @@ -4362,6 +4363,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", @@ -4687,6 +4689,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5978,6 +5981,7 @@ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6039,6 +6043,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6280,6 +6285,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", @@ -6750,7 +6756,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8247,7 +8252,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -8436,6 +8440,7 @@ "version": "0.33.3", "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", + "peer": true, "engines": { "node": ">=14.16" }, @@ -9335,6 +9340,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -9462,6 +9468,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10472,8 +10479,7 @@ "node_modules/string-format": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", - "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", - "peer": true + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==" }, "node_modules/string-width": { "version": "4.2.3", @@ -10846,7 +10852,6 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", - "peer": true, "dependencies": { "chalk": "^4.1.0", "command-line-args": "^5.1.1", @@ -10957,6 +10962,7 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -11023,7 +11029,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", - "peer": true, "dependencies": { "@types/prettier": "^2.1.1", "debug": "^4.3.1", @@ -11047,7 +11052,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11058,7 +11062,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "deprecated": "Glob versions prior to v9 are no longer supported", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11077,14 +11080,12 @@ "node_modules/typechain/node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "peer": true + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" }, "node_modules/typechain/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11180,6 +11181,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11251,7 +11253,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -11904,6 +11905,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "peer": true, "engines": { "node": ">= 8" } From d3f5bf91dfff76c6d1bb83123ba46fcb2aaa874b Mon Sep 17 00:00:00 2001 From: pennhan-dex Date: Tue, 13 Jan 2026 11:01:33 +0800 Subject: [PATCH 6/8] revert package.json --- package-lock.json | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8868ecd..392e9b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1366,7 +1366,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "peer": true, "dependencies": { "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", @@ -1760,7 +1759,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", @@ -4301,7 +4299,8 @@ "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "peer": true }, "node_modules/@types/signale": { "version": "1.4.7", @@ -4363,7 +4362,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", @@ -4689,7 +4687,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5981,7 +5978,6 @@ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6043,7 +6039,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6285,7 +6280,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "peer": true, "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", @@ -6756,6 +6750,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8252,6 +8247,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -8440,7 +8436,6 @@ "version": "0.33.3", "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", - "peer": true, "engines": { "node": ">=14.16" }, @@ -9340,7 +9335,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -9468,7 +9462,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10479,7 +10472,8 @@ "node_modules/string-format": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", - "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==" + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "peer": true }, "node_modules/string-width": { "version": "4.2.3", @@ -10852,6 +10846,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", + "peer": true, "dependencies": { "chalk": "^4.1.0", "command-line-args": "^5.1.1", @@ -10962,7 +10957,6 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -11029,6 +11023,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", + "peer": true, "dependencies": { "@types/prettier": "^2.1.1", "debug": "^4.3.1", @@ -11052,6 +11047,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11062,6 +11058,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "deprecated": "Glob versions prior to v9 are no longer supported", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11080,12 +11077,14 @@ "node_modules/typechain/node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "peer": true }, "node_modules/typechain/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11181,7 +11180,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11253,6 +11251,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -11905,7 +11904,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "peer": true, "engines": { "node": ">= 8" } From fe2dba0e90d0912f1f1fb839c66bac25549a1848 Mon Sep 17 00:00:00 2001 From: pennhan-dex Date: Tue, 13 Jan 2026 13:58:32 +0800 Subject: [PATCH 7/8] update readme --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index b009947..ce87216 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A command-line interface tool for working with Decentralized Identifiers (DIDs), - ✅ **Modern Cryptosuites**: Full support for ECDSA-SD-2023 and BBS-2023 - ✅ **Key Pair Generation**: Generate cryptographic key pairs with Multikey format - ✅ **DID Management**: Create and manage did:web identifiers +- ✅ **Sign Verifiable Credentials**: Sign verifiable credentials - ✅ **Token Registry**: Mint tokens to blockchain-based token registries - ✅ **Credential Status**: Create and update W3C credential status lists - ✅ **W3C Standards**: Compliant with latest W3C DID and Verifiable Credentials specifications @@ -26,6 +27,7 @@ This CLI leverages the TrustVC package: - [Commands](#commands) - [`trustvc key-pair-generation`](#trustvc-key-pair-generation) - [`trustvc did-web`](#trustvc-did-web) + - [`trustvc w3c-sign`](#trustvc-w3c-sign) - [`trustvc credential-status-create`](#trustvc-credential-status-create) - [`trustvc credential-status-update`](#trustvc-credential-status-update) - [`trustvc mint`](#trustvc-mint) @@ -56,6 +58,9 @@ trustvc key-pair-generation # Create a DID from the key pair trustvc did-web +# Sign a verifiable credential +trustvc w3c-sign + # Create a credential status list trustvc credential-status-create @@ -72,6 +77,8 @@ trustvc mint - **Generating Well-Known DID**: The CLI uses the `issueDID` function from `@trustvc/trustvc` to generate a did:web identifier. This allows users to self-host their DID as a unique identifier in decentralized systems. +- **Sign Verifiable Credentials**: The CLI uses the `w3cSign` function from `@trustvc/trustvc` to sign verifiable credentials with the provided did:web identifier. + - **Credential Status Management**: The CLI provides commands to create and update W3C credential status lists for managing the revocation status of verifiable credentials. - **Token Registry Minting**: The CLI uses the `mint` function from `@trustvc/trustvc` to mint document hashes to blockchain-based token registries, supporting multiple networks including Ethereum, Polygon, XDC, Stability, and Astron. @@ -117,6 +124,23 @@ Generates a did:web identifier from an existing key pair. Supports modern Multik trustvc did-web ``` +### `trustvc w3c-sign` + +Signs a verifiable credential using a did:web identifier. + +**Interactive prompts:** +- Path to did:web key-pair JSON file +- Path to unsigned verifiable credential JSON file +- Select cryptosuite (ECDSA-SD-2023 or BBS-2023, must match the key pair) +- Output directory + +**Output:** Creates a signed verifiable credential file: `signed_vc.json`. + +**Example:** +```sh +trustvc w3c-sign +``` + ### `trustvc credential-status-create` Creates a new W3C credential status list for managing the revocation status of verifiable credentials. @@ -255,6 +279,7 @@ npm test │ │ └── w3c/ │ │ ├── did.ts # DID generation command │ │ ├── key-pair.ts # Key pair generation command +│ │ ├── sign.ts # Sign verifiable credential command │ │ └── credentialStatus/ │ │ ├── create.ts # Create credential status │ │ └── update.ts # Update credential status @@ -272,6 +297,7 @@ npm test │ │ └── w3c/ │ │ ├── did.test.ts │ │ ├── key-pair.test.ts +│ │ ├── sign.test.ts │ │ └── credentialStatus/ │ │ ├── create.test.ts │ │ └── update.test.ts From a775b9d66a346052e8c10839ad92f2497beeff6a Mon Sep 17 00:00:00 2001 From: pennhan-dex Date: Wed, 14 Jan 2026 13:56:44 +0800 Subject: [PATCH 8/8] address comments (reorder prompt, add default path and success message) --- .gitignore | 4 ++ src/commands/w3c/sign.ts | 33 +++++++------ src/types.ts | 2 +- tests/commands/w3c/sign.test.ts | 87 +++++++++++++++++---------------- tests/main.test.ts | 44 +++++++++-------- 5 files changed, 89 insertions(+), 81 deletions(-) diff --git a/.gitignore b/.gitignore index c2e9990..b0c1ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ +# Sample files for testing +fixtures + # Generated files from CLI commands keypair.json didKeyPairs.json wellknown.json credentialStatus.json +signed_vc.json # Dependencies node_modules/ diff --git a/src/commands/w3c/sign.ts b/src/commands/w3c/sign.ts index bea335e..c3cdfd6 100644 --- a/src/commands/w3c/sign.ts +++ b/src/commands/w3c/sign.ts @@ -5,7 +5,7 @@ import { SignInput } from '../../types'; import signale from 'signale'; export const command = 'w3c-sign'; -export const describe = 'Sign a verifiable credential using a did key-pair file'; +export const describe = 'Sign a Verifiable Credential using a did key-pair file'; export const handler = async () => { try { @@ -19,31 +19,30 @@ export const handler = async () => { }; export const promptForInputs = async (): Promise => { - const pathToKeypairFile = await input({ - message: 'Please enter the path to your did key-pair JSON file:', + const pathToCredentialFile = await input({ + message: 'Please enter the path to your Verifiable Credential JSON file:', required: true, validate: (value: string) => { if (!value || value.trim() === '') { - return 'did key-pair JSON file path is required'; + return 'Verifiable Credential JSON file path is required'; } return true; }, }); + const credential: RawVerifiableCredential = readJsonFile(pathToCredentialFile, 'Verifiable Credential JSON'); - const keyPairData: typeof issuer.IssuedDIDOption = readJsonFile(pathToKeypairFile, 'key pair'); - - const pathToCredentialFile = await input({ - message: 'Pleaae enter the path to your credential JSON file:', + const pathToKeypairFile = await input({ + message: 'Please enter the path to your did key-pair JSON file:', required: true, + default: './didKeyPairs.json', validate: (value: string) => { if (!value || value.trim() === '') { - return 'Credential JSON file path is required'; + return 'did key-pair JSON file path is required'; } return true; }, }); - - const credential: RawVerifiableCredential = readJsonFile(pathToCredentialFile, 'credential JSON'); + const keyPairData: typeof issuer.IssuedDIDOption = readJsonFile(pathToKeypairFile, 'key pair'); const encryptionAlgorithm = await select({ message: 'Select the encryption algorithm used to generate the key pair:', @@ -55,31 +54,33 @@ export const promptForInputs = async (): Promise => { }); const pathToSignedVC = await input({ - message: 'Enter a directory to save the signed verifiable credential (optional):', - default: '.', + message: 'Enter a directory to save the signed Verifiable Credential (optional):', required: false, + default: '.', }); if (!isDirectoryValid(pathToSignedVC)) throw new Error('Output path is not valid'); return { - keyPairData, credential, + keyPairData, encryptionAlgorithm, pathToSignedVC, }; }; export const sign = async ({ - keyPairData, credential, + keyPairData, encryptionAlgorithm, pathToSignedVC, }: SignInput): Promise => { const signedVC = await signW3C(credential, keyPairData, encryptionAlgorithm); if (signedVC?.signed) { + signale.success('Verifiable Credential signed successfully'); const signedVCPath = `${pathToSignedVC}/signed_vc.json`; - writeFile(signedVCPath, signedVC.signed); + writeFile(signedVCPath, signedVC.signed, true); + signale.success(`Signed verifiable credential saved to: ${signedVCPath}`); } else { signale.error(signedVC.error); } diff --git a/src/types.ts b/src/types.ts index da95d80..a0cb233 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,8 +2,8 @@ import { credentialStatus, issuer, RawVerifiableCredential } from '@trustvc/trus import { GasOption, NetworkOption, RpcUrlOption, WalletOrSignerOption } from './utils'; export type SignInput = { - keyPairData: typeof issuer.IssuedDIDOption; credential: RawVerifiableCredential; + keyPairData: typeof issuer.IssuedDIDOption; encryptionAlgorithm: typeof credentialStatus.cryptoSuiteName; pathToSignedVC: string; } diff --git a/tests/commands/w3c/sign.test.ts b/tests/commands/w3c/sign.test.ts index 5312b3d..0bcdaf1 100644 --- a/tests/commands/w3c/sign.test.ts +++ b/tests/commands/w3c/sign.test.ts @@ -49,15 +49,15 @@ describe('w3c-sign', () => { describe('promptForInputs', () => { it('should return parsed inputs when algorithm is ecdsa-sd-2023', async () => { (prompts.input as any) - .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('.'); (prompts.select as any).mockResolvedValueOnce('ecdsa-sd-2023'); const utils = await import('../../../src/utils'); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); const result = await promptForInputs(); @@ -72,15 +72,15 @@ describe('w3c-sign', () => { it('should return parsed inputs when algorithm is bbs-2023', async () => { (prompts.input as any) - .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('.'); (prompts.select as any).mockResolvedValueOnce('bbs-2023'); const utils = await import('../../../src/utils'); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); const result = await promptForInputs(); @@ -95,47 +95,47 @@ describe('w3c-sign', () => { it('should abide by validation rules for path inputs', async () => { (prompts.input as any) - .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('.'); (prompts.select as any).mockResolvedValueOnce('bbs-2023'); const utils = await import('../../../src/utils'); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); await promptForInputs(); - const [keypairArgs, credentialArgs, signedVcArgs] = + const [credentialArgs, keypairArgs, signedVcArgs] = (prompts.input as any).mock.calls.map((c: any[]) => c[0]); + expect(credentialArgs.required).toBe(true); + expect(credentialArgs.validate('')).toBe('Verifiable Credential JSON file path is required'); + expect(credentialArgs.validate(' ')).toBe('Verifiable Credential JSON file path is required'); + expect(credentialArgs.validate('./credential.json')).toBe(true); + expect(keypairArgs.required).toBe(true); expect(keypairArgs.validate('')).toBe('did key-pair JSON file path is required'); expect(keypairArgs.validate(' ')).toBe('did key-pair JSON file path is required'); expect(keypairArgs.validate('./did-keypair.json')).toBe(true); - expect(credentialArgs.required).toBe(true); - expect(credentialArgs.validate('')).toBe('Credential JSON file path is required'); - expect(credentialArgs.validate(' ')).toBe('Credential JSON file path is required'); - expect(credentialArgs.validate('./credential.json')).toBe(true); - expect(signedVcArgs.required).toBe(false); expect(signedVcArgs.default).toBe('.'); }); it('should prompt for encryption algorithm with supported choices', async () => { (prompts.input as any) - .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('.'); (prompts.select as any).mockResolvedValueOnce('ecdsa-sd-2023'); const utils = await import('../../../src/utils'); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); await promptForInputs(); @@ -152,12 +152,16 @@ describe('w3c-sign', () => { }); it('should throw error when given an invalid did key-pair file path (readJsonFile fails)', async () => { - (prompts.input as any).mockResolvedValueOnce('./did-keypair.json'); + (prompts.input as any) + .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('./did-keypair.json'); const utils = await import('../../../src/utils'); - (utils.readJsonFile as MockedFunction).mockImplementation(() => { - throw new Error('Invalid key pair file path: ./did-keypair.json'); - }); + (utils.readJsonFile as MockedFunction) + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockImplementation(() => { + throw new Error('Invalid key pair file path: ./did-keypair.json'); + }); await expect(promptForInputs()).rejects.toThrow( 'Invalid key pair file path: ./did-keypair.json', @@ -165,17 +169,12 @@ describe('w3c-sign', () => { }); it('should throw error when given an invalid credential file path (readJsonFile fails)', async () => { - (prompts.input as any) - .mockResolvedValueOnce('./did-keypair.json') - .mockResolvedValueOnce('./credential.json'); + (prompts.input as any).mockResolvedValueOnce('./credential.json'); const utils = await import('../../../src/utils'); - (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockImplementation(() => { - throw new Error('Invalid credential JSON file path: ./credential.json'); - }); - (utils.isDirectoryValid as MockedFunction).mockReturnValue(true); + (utils.readJsonFile as MockedFunction).mockImplementation(() => { + throw new Error('Invalid credential JSON file path: ./credential.json'); + }); await expect(promptForInputs()).rejects.toThrow( 'Invalid credential JSON file path: ./credential.json', @@ -184,15 +183,15 @@ describe('w3c-sign', () => { it('should throw error when output path is not a valid directory', async () => { (prompts.input as any) - .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./credential.json') + .mockResolvedValueOnce('./did-keypair.json') .mockResolvedValueOnce('./invalid-dir'); (prompts.select as any).mockResolvedValueOnce('ecdsa-sd-2023'); const utils = await import('../../../src/utils'); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue(false); await expect(promptForInputs()).rejects.toThrow('Output path is not valid'); @@ -223,11 +222,6 @@ describe('w3c-sign', () => { const signale = await import('signale'); signaleSuccessMock = (signale.default as any).success; signaleErrorMock = (signale.default as any).error; - - writeFileMock.mockImplementation((...args: unknown[]) => { - const filePath = args[0] as string; - signaleSuccessMock(`Saved: ${filePath}`); - }); }); it('should sign with ecdsa-sd-2023 and writes to default output path', async () => { @@ -240,8 +234,11 @@ describe('w3c-sign', () => { input.keyPairData, 'ecdsa-sd-2023', ); - expect(writeFileMock).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }); - expect(signaleSuccessMock).toHaveBeenCalledWith('Saved: ./signed_vc.json'); + expect(writeFileMock).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }, true); + expect(signaleSuccessMock).toHaveBeenCalledWith('Verifiable Credential signed successfully'); + expect(signaleSuccessMock).toHaveBeenCalledWith( + 'Signed verifiable credential saved to: ./signed_vc.json', + ); expect(signaleErrorMock).not.toHaveBeenCalled(); }); @@ -259,8 +256,11 @@ describe('w3c-sign', () => { input.keyPairData, 'bbs-2023', ); - expect(writeFileMock).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }); - expect(signaleSuccessMock).toHaveBeenCalledWith('Saved: ./out/signed_vc.json'); + expect(writeFileMock).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }, true); + expect(signaleSuccessMock).toHaveBeenCalledWith('Verifiable Credential signed successfully'); + expect(signaleSuccessMock).toHaveBeenCalledWith( + 'Signed verifiable credential saved to: ./out/signed_vc.json', + ); expect(signaleErrorMock).not.toHaveBeenCalled(); }); @@ -282,6 +282,7 @@ describe('w3c-sign', () => { signW3CMock.mockResolvedValue({ signed: {} }); await expect(sign(input)).rejects.toThrow('Unexpected error while writing'); + expect(signaleSuccessMock).toHaveBeenCalledWith('Verifiable Credential signed successfully'); }); }); }); diff --git a/tests/main.test.ts b/tests/main.test.ts index 62d0b78..c8a34d0 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -390,10 +390,6 @@ describe('trustvc-cli', () => { writeFileSpy = vi.spyOn(utils, 'writeFile') as MockedFunction; signaleErrorSpy = signale.error as MockedFunction; signaleSuccessSpy = signale.success as MockedFunction; - - writeFileSpy.mockImplementation(((filePath: string) => { - signaleSuccessSpy(`Saved: ${filePath}`); - }) as any); }); it('should sign a credential using ecdsa-sd-2023 and write signed VC', async ({ expect }) => { @@ -405,14 +401,14 @@ describe('trustvc-cli', () => { }; (prompts.input as any) - .mockResolvedValueOnce(input.keyPairPath) .mockResolvedValueOnce(input.credentialPath) + .mockResolvedValueOnce(input.keyPairPath) .mockResolvedValueOnce(input.outputPath); (prompts.select as any).mockResolvedValueOnce(input.encryptionAlgorithm); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue( true, ); @@ -426,8 +422,11 @@ describe('trustvc-cli', () => { { domain: 'https://example.com' }, 'ecdsa-sd-2023', ); - expect(writeFileSpy).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }); - expect(signaleSuccessSpy).toHaveBeenCalledWith('Saved: ./signed_vc.json'); + expect(writeFileSpy).toHaveBeenCalledWith('./signed_vc.json', { proof: 'ok' }, true); + expect(signaleSuccessSpy).toHaveBeenCalledWith('Verifiable Credential signed successfully'); + expect(signaleSuccessSpy).toHaveBeenCalledWith( + 'Signed verifiable credential saved to: ./signed_vc.json', + ); expect(signaleErrorSpy).not.toHaveBeenCalled(); }); @@ -440,14 +439,14 @@ describe('trustvc-cli', () => { }; (prompts.input as any) - .mockResolvedValueOnce(input.keyPairPath) .mockResolvedValueOnce(input.credentialPath) + .mockResolvedValueOnce(input.keyPairPath) .mockResolvedValueOnce(input.outputPath); (prompts.select as any).mockResolvedValueOnce(input.encryptionAlgorithm); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue( true, ); @@ -461,8 +460,11 @@ describe('trustvc-cli', () => { { domain: 'https://example.com' }, 'bbs-2023', ); - expect(writeFileSpy).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }); - expect(signaleSuccessSpy).toHaveBeenCalledWith('Saved: ./out/signed_vc.json'); + expect(writeFileSpy).toHaveBeenCalledWith('./out/signed_vc.json', { proof: 'ok' }, true); + expect(signaleSuccessSpy).toHaveBeenCalledWith('Verifiable Credential signed successfully'); + expect(signaleSuccessSpy).toHaveBeenCalledWith( + 'Signed verifiable credential saved to: ./out/signed_vc.json', + ); expect(signaleErrorSpy).not.toHaveBeenCalled(); }); @@ -475,14 +477,14 @@ describe('trustvc-cli', () => { }; (prompts.input as any) - .mockResolvedValueOnce(input.keyPairPath) .mockResolvedValueOnce(input.credentialPath) + .mockResolvedValueOnce(input.keyPairPath) .mockResolvedValueOnce(input.outputPath); (prompts.select as any).mockResolvedValueOnce(input.encryptionAlgorithm); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue( false, ); @@ -504,14 +506,14 @@ describe('trustvc-cli', () => { }; (prompts.input as any) - .mockResolvedValueOnce(input.keyPairPath) .mockResolvedValueOnce(input.credentialPath) + .mockResolvedValueOnce(input.keyPairPath) .mockResolvedValueOnce(input.outputPath); (prompts.select as any).mockResolvedValueOnce(input.encryptionAlgorithm); (utils.readJsonFile as MockedFunction) - .mockReturnValueOnce({ domain: 'https://example.com' }) - .mockReturnValueOnce({ id: 'urn:uuid:123' }); + .mockReturnValueOnce({ id: 'urn:uuid:123' }) + .mockReturnValueOnce({ domain: 'https://example.com' }); (utils.isDirectoryValid as MockedFunction).mockReturnValue( true, ); @@ -524,7 +526,7 @@ describe('trustvc-cli', () => { await signHandler(); expect(signW3CSpy).toHaveBeenCalled(); - expect(signaleSuccessSpy).not.toHaveBeenCalled(); + expect(signaleSuccessSpy).toHaveBeenCalledWith('Verifiable Credential signed successfully'); expect(signaleErrorSpy).toHaveBeenCalledWith('Unable to write file to ./signed_vc.json'); }); });